diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 91581d4..36ed5ac 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -6,8 +6,8 @@ jobs:
   ubuntu-build-icebreaker-r31:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
-      - uses: YosysHQ/setup-oss-cad-suite@v2
+      - uses: actions/checkout@v4
+      - uses: YosysHQ/setup-oss-cad-suite@v3
       - run: git submodule update --init gateware/external/no2misc
       - run: yosys --version
       - run: make HW_REV=HW_R31 BOARD=icebreaker CORE=mirror -C gateware
@@ -19,8 +19,8 @@ jobs:
   ubuntu-build-icebreaker-r33:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
-      - uses: YosysHQ/setup-oss-cad-suite@v2
+      - uses: actions/checkout@v4
+      - uses: YosysHQ/setup-oss-cad-suite@v3
       - run: git submodule update --init gateware/external/no2misc
       - run: yosys --version
       - run: make HW_REV=HW_R33 BOARD=icebreaker CORE=mirror -C gateware
@@ -29,6 +29,19 @@ jobs:
           name: ubuntu-build-icebreaker.bin
           path: gateware/build/icebreaker/top.bin
 
+  ubuntu-build-icebreaker-r33-touch:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: YosysHQ/setup-oss-cad-suite@v3
+      - run: git submodule update --init gateware/external/no2misc
+      - run: yosys --version
+      - run: make HW_REV=HW_R33 BOARD=icebreaker CORE=touch_cv TOUCH=TOUCH_SENSE_ENABLED -C gateware
+      - uses: actions/upload-artifact@v3
+        with:
+          name: ubuntu-build-icebreaker.bin
+          path: gateware/build/icebreaker/top.bin
+
   windows-build-icebreaker:
     runs-on: windows-latest
     defaults:
@@ -40,8 +53,8 @@ jobs:
           install: >-
             git
             make
-      - uses: actions/checkout@v3
-      - uses: YosysHQ/setup-oss-cad-suite@v2
+      - uses: actions/checkout@v4
+      - uses: YosysHQ/setup-oss-cad-suite@v3
       - run: git submodule update --init gateware/external/no2misc
       - run: |
           export PATH=$PATH:$RUNNER_TEMP/oss-cad-suite/bin
@@ -56,8 +69,8 @@ jobs:
   macos-build-icebreaker:
     runs-on: macos-latest
     steps:
-      - uses: actions/checkout@v3
-      - uses: YosysHQ/setup-oss-cad-suite@v2
+      - uses: actions/checkout@v4
+      - uses: YosysHQ/setup-oss-cad-suite@v3
       - run: git submodule update --init gateware/external/no2misc
       - run: |
           yosys --version
@@ -70,8 +83,8 @@ jobs:
   ubuntu-build-colorlight-i5:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
-      - uses: YosysHQ/setup-oss-cad-suite@v2
+      - uses: actions/checkout@v4
+      - uses: YosysHQ/setup-oss-cad-suite@v3
       - run: git submodule update --init gateware/external/no2misc
       - run: yosys --version
       - run: make HW_REV=HW_R33 BOARD=colorlight_i5 CORE=mirror -C gateware
@@ -83,8 +96,8 @@ jobs:
   ubuntu-build-colorlight-i9:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
-      - uses: YosysHQ/setup-oss-cad-suite@v2
+      - uses: actions/checkout@v4
+      - uses: YosysHQ/setup-oss-cad-suite@v3
       - run: git submodule update --init gateware/external/no2misc
       - run: yosys --version
       - run: make HW_REV=HW_R33 BOARD=colorlight_i9 CORE=mirror -C gateware
@@ -96,8 +109,8 @@ jobs:
   ubuntu-build-ecpix-5:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
-      - uses: YosysHQ/setup-oss-cad-suite@v2
+      - uses: actions/checkout@v4
+      - uses: YosysHQ/setup-oss-cad-suite@v3
       - run: git submodule update --init gateware/external/no2misc
       - run: yosys --version
       - run: make HW_REV=HW_R33 BOARD=ecpix5 CORE=mirror -C gateware
@@ -109,8 +122,8 @@ jobs:
   ubuntu-build-pico-ice:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
-      - uses: YosysHQ/setup-oss-cad-suite@v2
+      - uses: actions/checkout@v4
+      - uses: YosysHQ/setup-oss-cad-suite@v3
       - run: git submodule update --init gateware/external/no2misc
       - run: yosys --version
       - run: make HW_REV=HW_R33 BOARD=pico_ice CORE=mirror -C gateware
@@ -122,8 +135,8 @@ jobs:
   run-tests:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
-      - uses: YosysHQ/setup-oss-cad-suite@v2
+      - uses: actions/checkout@v4
+      - uses: YosysHQ/setup-oss-cad-suite@v3
       - run: git submodule update --init gateware/external/no2misc
       - run: cocotb-config -v
       - run: cd gateware/sim && ./00_run.sh
@@ -131,8 +144,8 @@ jobs:
   run-linter:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
-      - uses: YosysHQ/setup-oss-cad-suite@v2
+      - uses: actions/checkout@v4
+      - uses: YosysHQ/setup-oss-cad-suite@v3
       - run: git submodule update --init gateware/external/no2misc
       - run: verilator --version
       - run: cd gateware && scripts/verilator_lint.sh
diff --git a/gateware/Makefile b/gateware/Makefile
index baf7592..76488ed 100644
--- a/gateware/Makefile
+++ b/gateware/Makefile
@@ -1,14 +1,17 @@
 ALL_BOARDS = $(shell ls boards)
 ALL_CORES = $(shell basename --suffix=.sv -- cores/*.sv)
 ALL_HW_REV = "HW_R31 HW_R33"
+ALL_TOUCH = "TOUCH_SENSE_DISABLED TOUCH_SENSE_ENABLED"
 
 CORE ?= mirror
+TOUCH ?= TOUCH_SENSE_DISABLED
 
 all prog:
 ifeq ($(BOARD),)
 	@echo "Valid HW_REV values are: $(ALL_HW_REV)".
 	@echo "Valid BOARD values are: $(ALL_BOARDS)".
 	@echo "Valid CORE values are: $(ALL_CORES)".
+	@echo "Valid TOUCH values are: $(ALL_TOUCH) (default disabled, valid for R3.3+ only, required for any examples that use touch)".
 	@echo "For example:"
 	@echo "  $$ make clean"
 	@echo "  $$ # Build bitstream with specific core and program it"
@@ -33,7 +36,7 @@ endif
 	mkdir -p build/$(BOARD)
 	# For now we always force a re-build since we can pass different DSP cores
 	# through environment vars and we need a re-build to happen in this case.
-	$(MAKE) -B -f boards/$(BOARD)/Makefile BUILD=build/$(BOARD) CORE=$(CORE) $(MAKECMDGOALS)
+	$(MAKE) -B -f boards/$(BOARD)/Makefile BUILD=build/$(BOARD) CORE=$(CORE) TOUCH=$(TOUCH) $(MAKECMDGOALS)
 
 clean:
 	rm -rf build/
diff --git a/gateware/cal/cal.py b/gateware/cal/cal.py
index ed43ce8..b41798f 100755
--- a/gateware/cal/cal.py
+++ b/gateware/cal/cal.py
@@ -112,9 +112,17 @@ def run_calibration(self):
                 "eeprom_dev": raw[3],
                 "eeprom_serial": int.from_bytes(raw[4:8], "big"),
                 "jack": raw[8],
+                "touch0": raw[9],
+                "touch1": raw[10],
+                "touch2": raw[11],
+                "touch3": raw[12],
+                "touch4": raw[13],
+                "touch5": raw[14],
+                "touch6": raw[15],
+                "touch7": raw[16],
             }
             [print(k, hex(v)) for k, v in values.items()]
-            self._decode_raw_samples(raw[9:])
+            self._decode_raw_samples(raw[17:])
             self._handle_user_input()
             self._calculate_calibration_strings()
             time.sleep(0.1)
diff --git a/gateware/cal/debug_uart.sv b/gateware/cal/debug_uart.sv
index 5900e1e..5962a2e 100644
--- a/gateware/cal/debug_uart.sv
+++ b/gateware/cal/debug_uart.sv
@@ -19,6 +19,14 @@ module debug_uart #(
     input [7:0] eeprom_dev,
     input [31:0] eeprom_serial,
     input [7:0] jack,
+    input [7:0] touch0,
+    input [7:0] touch1,
+    input [7:0] touch2,
+    input [7:0] touch3,
+    input [7:0] touch4,
+    input [7:0] touch5,
+    input [7:0] touch6,
+    input [7:0] touch7,
     input signed [W-1:0] adc0,
     input signed [W-1:0] adc1,
     input signed [W-1:0] adc2,
@@ -70,31 +78,39 @@ always_ff @(posedge clk) begin
             6:   dout <= eeprom_serial[32-2*8-1:32-3*8];
             7:   dout <= eeprom_serial[32-3*8-1:     0];
             8:   dout <= jack;
+            9:   dout <= touch0;
+            10:  dout <= touch1;
+            11:  dout <= touch2;
+            12:  dout <= touch3;
+            13:  dout <= touch4;
+            14:  dout <= touch5;
+            15:  dout <= touch6;
+            16:  dout <= touch7;
             // Channel 0
-            9:   dout <= adc0_ex[WM    -1:WM-1*8];
-            10:  dout <= adc0_ex[WM-1*8-1:WM-2*8];
-            11:  dout <= adc0_ex[WM-2*8-1:WM-3*8];
-            12:  dout <= adc0_ex[WM-3*8-1:     0];
+            17:   dout <= adc0_ex[WM    -1:WM-1*8];
+            18:  dout <= adc0_ex[WM-1*8-1:WM-2*8];
+            19:  dout <= adc0_ex[WM-2*8-1:WM-3*8];
+            20:  dout <= adc0_ex[WM-3*8-1:     0];
             // Channel 1
-            13:  dout <= adc1_ex[WM    -1:WM-1*8];
-            14:  dout <= adc1_ex[WM-1*8-1:WM-2*8];
-            15:  dout <= adc1_ex[WM-2*8-1:WM-3*8];
-            16:  dout <= adc1_ex[WM-3*8-1:     0];
+            21:  dout <= adc1_ex[WM    -1:WM-1*8];
+            22:  dout <= adc1_ex[WM-1*8-1:WM-2*8];
+            23:  dout <= adc1_ex[WM-2*8-1:WM-3*8];
+            24:  dout <= adc1_ex[WM-3*8-1:     0];
             // Channel 2
-            17:  dout <= adc2_ex[WM    -1:WM-1*8];
-            18:  dout <= adc2_ex[WM-1*8-1:WM-2*8];
-            19:  dout <= adc2_ex[WM-2*8-1:WM-3*8];
-            20:  dout <= adc2_ex[WM-3*8-1:     0];
+            25:  dout <= adc2_ex[WM    -1:WM-1*8];
+            26:  dout <= adc2_ex[WM-1*8-1:WM-2*8];
+            27:  dout <= adc2_ex[WM-2*8-1:WM-3*8];
+            28:  dout <= adc2_ex[WM-3*8-1:     0];
             // Channel 3
-            21:  dout <= adc3_ex[WM    -1:WM-1*8];
-            22:  dout <= adc3_ex[WM-1*8-1:WM-2*8];
-            23:  dout <= adc3_ex[WM-2*8-1:WM-3*8];
-            24:  dout <= adc3_ex[WM-3*8-1:     0];
+            29:  dout <= adc3_ex[WM    -1:WM-1*8];
+            30:  dout <= adc3_ex[WM-1*8-1:WM-2*8];
+            31:  dout <= adc3_ex[WM-2*8-1:WM-3*8];
+            32:  dout <= adc3_ex[WM-3*8-1:     0];
             default: begin
                 // Should never get here
             end
         endcase
-        if (state != 24) state <= state + 1;
+        if (state != 32) state <= state + 1;
         else state <= 0;
     end
 end
diff --git a/gateware/cores/touch_cv.sv b/gateware/cores/touch_cv.sv
new file mode 100644
index 0000000..5662176
--- /dev/null
+++ b/gateware/cores/touch_cv.sv
@@ -0,0 +1,43 @@
+//
+// Touch-to-CV. Touches on input jacks 1-4 are
+// translated into CV outputs on outputs 1-4.
+//
+// When building with touch sensing, be sure to build with
+// TOUCH=TOUCH_SENSE_ENABLED, for example:
+//
+// $ make CORE=touch_cv HW_REV=HW_R33 TOUCH=TOUCH_SENSE_ENABLED
+//
+
+`default_nettype none
+
+module touch_cv #(
+    parameter W = 16
+)(
+    input rst,
+    input clk,
+    input sample_clk,
+    input signed [W-1:0] sample_in0,
+    input signed [W-1:0] sample_in1,
+    input signed [W-1:0] sample_in2,
+    input signed [W-1:0] sample_in3,
+    output signed [W-1:0] sample_out0,
+    output signed [W-1:0] sample_out1,
+    output signed [W-1:0] sample_out2,
+    output signed [W-1:0] sample_out3,
+    input [7:0] jack,
+    input [7:0] touch0,
+    input [7:0] touch1,
+    input [7:0] touch2,
+    input [7:0] touch3,
+    input [7:0] touch4,
+    input [7:0] touch5,
+    input [7:0] touch6,
+    input [7:0] touch7
+);
+
+assign sample_out0 = W'(touch0) <<< (W-10);
+assign sample_out1 = W'(touch1) <<< (W-10);
+assign sample_out2 = W'(touch2) <<< (W-10);
+assign sample_out3 = W'(touch3) <<< (W-10);
+
+endmodule
diff --git a/gateware/drivers/cy8cmbr3108-cfg.hex b/gateware/drivers/cy8cmbr3108-cfg.hex
new file mode 100644
index 0000000..a0e928c
--- /dev/null
+++ b/gateware/drivers/cy8cmbr3108-cfg.hex
@@ -0,0 +1,130 @@
+6e
+00
+ff
+00
+00
+00
+00
+00
+00
+00
+ff
+ff
+00
+00
+80
+80
+80
+80
+80
+80
+80
+80
+00
+00
+00
+00
+00
+00
+00
+00
+04
+9f
+00
+B2
+94
+94
+00
+00
+00
+00
+00
+80
+05
+00
+00
+02
+00
+02
+00
+00
+00
+00
+00
+00
+00
+1e
+1e
+00
+00
+1e
+1e
+00
+00
+00
+01
+01
+00
+ff
+ff
+ff
+ff
+00
+00
+00
+00
+00
+00
+00
+10
+03
+00
+20
+00
+37
+01
+0f
+00
+0a
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+00
+86
+c1
diff --git a/gateware/drivers/cy8cmbr3108.py b/gateware/drivers/cy8cmbr3108.py
new file mode 100755
index 0000000..9f775bb
--- /dev/null
+++ b/gateware/drivers/cy8cmbr3108.py
@@ -0,0 +1,170 @@
+#!/bin/python3
+
+"""
+Touch IC utility -- verify the register indices and CRC in `cy8cmbr3108.hex`.
+This was mostly just pulled out of Cypress' documentation. They have tools
+to create the configuration dumps, however you aren't allowed to touch every
+register which I needed to do for this use-case :)
+
+If you run this utility from this directory, you'll get something like:
+
+```
+head 6e int 110
+head 00 int 0
+reg 0x0 hex 0xff int 255 SENSOR_EN
+reg 0x1 hex 0x0 int 0
+reg 0x2 hex 0x0 int 0 FSS_EN
+reg 0x3 hex 0x0 int 0
+...
+reg 0x7d hex 0x0 int 0
+reg 0x7e hex 0x86 int 134 CONFIG_CRC
+reg 0x7f hex 0xc1 int 193
+total bytes in file 130
+bytes to crc 126
+crc0 0x86
+crc1 0xc1
+CRC OK
+```
+
+If the CRC did not match (i.e you tweaked a register), you should copy the crc0 and crc1 (calculated CRC)
+lines into the correct lines of the .hex file (CONFIG_CRC above). Then, if you re-run this tool it should
+show CRC OK, which means the touch IC will accept your configuration.
+
+"""
+
+CY8CMBR3xxx_CONFIG_DATA_LENGTH = 126
+CY8CMBR3xxx_CRC_BIT_WIDTH = 2 * 8
+CY8CMBR3xxx_CRC_BIT4_MASK = 0x0F
+CY8CMBR3xxx_CRC_BIT4_SHIFT = 4
+CY8CMBR3xxx_CCITT16_DEFAULT_SEED = 0xffff
+CY8CMBR3xxx_CCITT16_POLYNOM = 0x1021
+
+
+def CY8CMBR3xxx_Calc4BitsCRC(value, remainder):
+    # Divide the value by polynomial, via the CRC polynomial
+    tableIndex = (value & CY8CMBR3xxx_CRC_BIT4_MASK) ^ (remainder >> (CY8CMBR3xxx_CRC_BIT_WIDTH - CY8CMBR3xxx_CRC_BIT4_SHIFT))
+    remainder = (CY8CMBR3xxx_CCITT16_POLYNOM * tableIndex) ^ (remainder << CY8CMBR3xxx_CRC_BIT4_SHIFT)
+    return remainder
+
+
+def CY8CMBR3xxx_CalculateCrc(configuration):
+    seed = CY8CMBR3xxx_CCITT16_DEFAULT_SEED
+
+    # don't make count down cycle! CRC will be different!
+    for byteValue in configuration:
+        seed = CY8CMBR3xxx_Calc4BitsCRC(byteValue >> CY8CMBR3xxx_CRC_BIT4_SHIFT, seed) & 0xffff
+        seed = CY8CMBR3xxx_Calc4BitsCRC(byteValue, seed) & 0xffff
+
+    return seed
+
+# From the datasheet for this chip, offsets of each register.
+REG_MAP = """SENSOR_EN 0x00
+FSS_EN 0x02
+TOGGLE_EN 0x04
+LED_ON_EN 0x06
+SENSITIVITY0 0x08
+SENSITIVITY1 0x09
+SENSITIVITY2 0x0a
+SENSITIVITY3 0x0b
+BASE_THRESHOLD0 0x0c
+BASE_THRESHOLD1 0x0d
+FINGER_THRESHOLD2 0x0e
+FINGER_THRESHOLD3 0x0f
+FINGER_THRESHOLD4 0x10
+FINGER_THRESHOLD5 0x11
+FINGER_THRESHOLD6 0x12
+FINGER_THRESHOLD7 0x13
+FINGER_THRESHOLD8 0x14
+FINGER_THRESHOLD9 0x15
+FINGER_THRESHOLD10 0x16
+FINGER_THRESHOLD11 0x17
+FINGER_THRESHOLD12 0x18
+FINGER_THRESHOLD13 0x19
+FINGER_THRESHOLD14 0x1a
+FINGER_THRESHOLD15 0x1b
+SENSOR_DEBOUNCE 0x1c
+BUTTON_HYS 0x1d
+BUTTON_LBR 0x1f
+BUTTON_NNT 0x20
+BUTTON_NT 0x21
+PROX_EN 0x26
+PROX_CFG 0x27
+PROX_CFG2 0x28
+PROX_TOUCH_TH0 0x2a
+PROX_TOUCH_TH1 0x2c
+PROX_RESOLUTION0 0x2e
+PROX_RESOLUTION1 0x2f
+PROX_HYS 0x30
+PROX_LBR 0x32
+PROX_NNT 0x33
+PROX_NT 0x34
+PROX_POSITIVE_TH0 0x35
+PROX_POSITIVE_TH1 0x36
+PROX_NEGATIVE_TH0 0x39
+PROX_NEGATIVE_TH1 0x3a
+LED_ON_TIME 0x3d
+BUZZER_CFG 0x3e
+BUZZER_ON_TIME 0x3f
+GPO_CFG 0x40
+PWM_DUTYCYCLE_CFG0 0x41
+PWM_DUTYCYCLE_CFG1 0x42
+PWM_DUTYCYCLE_CFG2 0x43
+PWM_DUTYCYCLE_CFG3 0x44
+PWM_DUTYCYCLE_CFG4 0x45
+PWM_DUTYCYCLE_CFG5 0x46
+PWM_DUTYCYCLE_CFG6 0x47
+PWM_DUTYCYCLE_CFG7 0x48
+SPO_CFG 0x4c
+DEVICE_CFG0 0x4d
+DEVICE_CFG1 0x4e
+DEVICE_CFG2 0x4f
+DEVICE_CFG3 0x50
+I2C_ADDR 0x51
+REFRESH_CTRL 0x52
+STATE_TIMEOUT 0x55
+SLIDER_CFG 0x5d
+SLIDER1_CFG 0x61
+SLIDER1_RESOLUTION 0x62
+SLIDER1_THRESHOLD 0x63
+SLIDER2_CFG 0x67
+SLIDER2_RESOLUTION 0x68
+SLIDER2_THRESHOLD 0x69
+SLIDER_LBR 0x71
+SLIDER_NNT 0x72
+SLIDER_NT 0x73
+SCRATCHPAD0 0x7a
+SCRATCHPAD1 0x7b
+CONFIG_CRC 0x7e"""
+
+REG_DICT = {}
+
+for line in REG_MAP.split("\n"):
+    name, addr = line.split(" ")
+    REG_DICT[int(addr.strip(), 16)] = name.strip()
+
+with open("cy8cmbr3108-cfg.hex", "r") as f:
+    xs = []
+    ix = 0
+    for line in f.readlines():
+        raw = line.strip()
+        v = int(raw, 16)
+        if ix >= 2:
+            reg_ix = ix-2
+            name = ""
+            if reg_ix in REG_DICT:
+                name = REG_DICT[reg_ix]
+            print("reg", hex(reg_ix), "hex", hex(int(raw, 16)), "int", v, name)
+        else:
+            print("head", raw, "int", v)
+        xs.append(v)
+        ix += 1
+    print("total bytes in file", len(xs))
+    xs_crc = xs[2:-2]
+    print("bytes to crc", len(xs_crc))
+    crc = CY8CMBR3xxx_CalculateCrc(xs_crc)
+    print("crc0", hex(crc & 0x00FF))
+    print("crc1", hex((crc & 0xFF00)>>8))
+    if xs[-2] == crc & 0x00FF and xs[-1] == ((crc & 0xFF00) >> 8):
+        print("CRC OK")
+    else:
+        print("CRC NOT OK")
diff --git a/gateware/drivers/pmod_i2c_master.sv b/gateware/drivers/pmod_i2c_master.sv
index d533d11..2746e0f 100644
--- a/gateware/drivers/pmod_i2c_master.sv
+++ b/gateware/drivers/pmod_i2c_master.sv
@@ -15,10 +15,14 @@
 `default_nettype none
 
 module pmod_i2c_master #(
-    parameter CODEC_CFG  = "drivers/ak4619-cfg.hex",
-    parameter CODEC_CFG_BYTES = 16'd23,
-    parameter LED_CFG  = "drivers/pca9635-cfg.hex",
-    parameter LED_CFG_BYTES = 16'd26
+      parameter CODEC_CFG  = "drivers/ak4619-cfg.hex"
+    , parameter CODEC_CFG_BYTES = 16'd23
+    , parameter LED_CFG  = "drivers/pca9635-cfg.hex"
+    , parameter LED_CFG_BYTES = 16'd26
+`ifdef TOUCH_SENSE_ENABLED
+    , parameter TOUCH_CFG  = "drivers/cy8cmbr3108-cfg.hex"
+    , parameter TOUCH_CFG_BYTES = 16'd130 // 0x80 + 2
+`endif
 )(
     input  clk,
     input  rst,
@@ -42,6 +46,17 @@ module pmod_i2c_master #(
     input signed [7:0] led6,
     input signed [7:0] led7,
 
+`ifdef TOUCH_SENSE_ENABLED
+    output logic [7:0] touch0,
+    output logic [7:0] touch1,
+    output logic [7:0] touch2,
+    output logic [7:0] touch3,
+    output logic [7:0] touch4,
+    output logic [7:0] touch5,
+    output logic [7:0] touch6,
+    output logic [7:0] touch7,
+`endif // TOUCH_SENSE_ENABLED
+
     // Jack detection outputs, 1 == inserted. (bit 0 is input 0, bit 4 is output 0).
     output logic [7:0] jack,
 
@@ -52,20 +67,24 @@ module pmod_i2c_master #(
 );
 
 // Overall state machine of this core.
-// Basically we bring up the EEPROM and CODEC, and then proceed to
+// Basically we bring up the EEPROM, touch sensing and CODEC, and then proceed to
 // update the LED outputs and read the jack insertion GPIOS in a loop.
 localparam I2C_DELAY1        = 0,
            I2C_EEPROM1       = 1,
            I2C_EEPROM2       = 2,
            I2C_INIT_TOUCH1   = 3,
            I2C_INIT_TOUCH2   = 4,
-           I2C_INIT_CODEC1   = 5,
-           I2C_INIT_CODEC2   = 6,
-           I2C_LED1          = 7,  // <<--\ LED/JACK re-runs indefinitely.
-           I2C_LED2          = 8,  //     |
-           I2C_JACK1         = 9,  //     |
-           I2C_JACK2         = 10, // >>--/
-           I2C_IDLE          = 11;
+           I2C_INIT_TOUCH3   = 5,
+           I2C_INIT_TOUCH4   = 6,
+           I2C_INIT_CODEC1   = 7,
+           I2C_INIT_CODEC2   = 8,
+           I2C_LED1          = 9,  // <<--\ LED/JACK/TOUCH re-runs indefinitely.
+           I2C_LED2          = 10, //     |
+           I2C_JACK1         = 11, //     |
+           I2C_JACK2         = 12, //     |
+           I2C_TOUCH_SCAN1   = 13, //     |  | these 2 only if TOUCH is enabled
+           I2C_TOUCH_SCAN2   = 14, // >>--/  |
+           I2C_IDLE          = 15;
 
 `ifdef COCOTB_SIM
 localparam STARTUP_DELAY_BIT = 4;
@@ -88,6 +107,15 @@ initial $readmemh(LED_CFG, led_config);
 // Index at which PWM values start in the led config.
 localparam PCA9635_PWM0 = 4;
 
+`ifdef TOUCH_SENSE_ENABLED
+// Logic for startup configuration of touch sensor IC over I2C.
+logic [7:0] touch_config [0:TOUCH_CFG_BYTES-1];
+initial $readmemh(TOUCH_CFG, touch_config);
+
+// Which touch sensor we are currently reading
+logic [2:0] nsensor;
+`endif
+
 // Valid commands for `i2c_master` core.
 localparam [1:0] I2CMASTER_START = 2'b00,
                  I2CMASTER_STOP  = 2'b01,
@@ -181,11 +209,152 @@ always_ff @(posedge clk) begin
                     i2c_state <= I2C_INIT_TOUCH2;
                     i2c_config_pos <= 0;
                 end
-                // Switch off the CY8CMBR3108 by default, as it can cause the
-                // LEDs to flicker (due to NACKs) and increase noise in the
-                // audio chain, unless it is configured correctly (currently
-                // touch sensing prototyping is on a separate branch, let's
-                // keep it out of master for now)
+`ifdef TOUCH_SENSE_ENABLED
+                // If touch sensing is enabled, send out one long transaction
+                // with configuration data, then issue a SAVE_CHECK_CRC cmd.
+                I2C_INIT_TOUCH2: begin
+                    case (i2c_config_pos)
+                        default: begin
+                            data_in <= touch_config[8'(i2c_config_pos)];
+                            cmd <= I2CMASTER_WRITE;
+                        end
+                        1: begin
+                            // Make sure the first byte (address) is acknowledged. If it
+                            // isn't, restart the configuration process.
+                            if (ack_out) begin
+                                i2c_state <= I2C_INIT_TOUCH1;
+                                cmd <= I2CMASTER_STOP;
+                            end else begin
+                                data_in <= touch_config[8'(i2c_config_pos)];
+                                cmd <= I2CMASTER_WRITE;
+                            end
+                        end
+                        TOUCH_CFG_BYTES: begin
+                            cmd <= I2CMASTER_STOP;
+                        end
+
+                        TOUCH_CFG_BYTES+1: begin
+                            cmd <= I2CMASTER_START;
+                        end
+                        TOUCH_CFG_BYTES+2: begin
+                            // 0x37 << 1 | 0 (W)
+                            data_in <= 8'h6E;
+                            cmd <= I2CMASTER_WRITE;
+                        end
+                        TOUCH_CFG_BYTES+3: begin
+                            // Command register
+                            data_in <= 8'h86;
+                            cmd <= I2CMASTER_WRITE;
+                        end
+                        TOUCH_CFG_BYTES+4: begin
+                            // SAVE_CHECK_CRC.
+                            data_in <= 8'h02;
+                            cmd <= I2CMASTER_WRITE;
+                        end
+                        TOUCH_CFG_BYTES+5: begin
+                            cmd <= I2CMASTER_STOP;
+                            i2c_state <= I2C_INIT_TOUCH3;
+                        end
+                    endcase
+                    i2c_config_pos <= i2c_config_pos + 1;
+                    ack_in <= 1'b1;
+                    stb <= 1'b1;
+                end
+                I2C_INIT_TOUCH3: begin
+                    cmd <= I2CMASTER_START;
+                    stb <= 1'b1;
+                    i2c_state <= I2C_INIT_TOUCH4;
+                    i2c_config_pos <= 0;
+                end
+                // Finally we issue a SW_RESET command to reset the touch
+                // sense IC and apply all the settings we just sent.
+                I2C_INIT_TOUCH4: begin
+                    case (i2c_config_pos)
+                        // Write the slave register pointer
+                        0: begin
+                            cmd <= I2CMASTER_START;
+                        end
+                        1: begin
+                            // 0x37 << 1 | 0 (W)
+                            data_in <= 8'h6E;
+                            cmd <= I2CMASTER_WRITE;
+                        end
+                        2: begin
+                            if (ack_out) begin
+                                // Wait until ack succeeds before continuing
+                                i2c_state <= I2C_INIT_TOUCH3;
+                                cmd <= I2CMASTER_STOP;
+                            end else begin
+                                // Command register
+                                data_in <= 8'h86;
+                                cmd <= I2CMASTER_WRITE;
+                            end
+                        end
+                        3: begin
+                            cmd <= I2CMASTER_STOP;
+                        end
+
+                        // Read the command register, retry if chip is busy
+                        4: begin
+                            cmd <= I2CMASTER_START;
+                        end
+                        5: begin
+                            // 0x37 << 1 | 1 (R)
+                            data_in <= 8'h6F;
+                            cmd <= I2CMASTER_WRITE;
+                        end
+                        6: begin
+                            cmd <= I2CMASTER_READ;
+                        end
+                        7: begin
+                            if (data_out != 8'h00) begin
+                                // Retry until command register is 0 before
+                                // issuing a reset.
+                                i2c_state <= I2C_INIT_TOUCH3;
+                            end
+                            cmd <= I2CMASTER_STOP;
+                        end
+
+
+                        // Write the command register
+                        8: begin
+                            cmd <= I2CMASTER_START;
+                        end
+                        9: begin
+                            // 0x37 << 1 | 0 (W)
+                            data_in <= 8'h6E;
+                            cmd <= I2CMASTER_WRITE;
+                        end
+                        10: begin
+                            if (ack_out) begin
+                                // Wait until ack succeeds before continuing
+                                i2c_state <= I2C_INIT_TOUCH3;
+                                cmd <= I2CMASTER_STOP;
+                            end else begin
+                                // Only issue reset if we got acknowledged
+                                // Command register
+                                data_in <= 8'h86;
+                                cmd <= I2CMASTER_WRITE;
+                            end
+                        end
+                        11: begin
+                            // NVM write & reset command.
+                            data_in <= 8'hff;
+                            cmd <= I2CMASTER_WRITE;
+                        end
+                        default: begin
+                            cmd <= I2CMASTER_STOP;
+                            i2c_state <= I2C_INIT_CODEC1;
+                        end
+                    endcase
+                    i2c_config_pos <= i2c_config_pos + 1;
+                    ack_in <= 1'b1;
+                    stb <= 1'b1;
+                end
+`else
+                // If touch sensing is not enabled, we disable the touch sense
+                // IC. This also improves noise performance a little, so might
+                // be desired for some audio scenarios.
                 I2C_INIT_TOUCH2: begin
                     case (i2c_config_pos)
                         0: begin
@@ -220,6 +389,7 @@ always_ff @(posedge clk) begin
                     ack_in <= 1'b1;
                     stb <= 1'b1;
                 end
+`endif // TOUCH_SENSE_ENABLED
                 I2C_INIT_CODEC1: begin
                     cmd <= I2CMASTER_START;
                     stb <= 1'b1;
@@ -308,13 +478,23 @@ always_ff @(posedge clk) begin
                             data_in <= 8'h31;
                             cmd <= I2CMASTER_WRITE;
                         end
-                        9: cmd <= I2CMASTER_READ;
-
+                        9: begin
+                            if (ack_out == 1'b0) begin
+                                cmd <= I2CMASTER_READ;
+                            end else begin
+                                cmd <= I2CMASTER_STOP;
+                                i2c_state <= I2C_TOUCH_SCAN1;
+                            end
+                        end
                         // 4) Save the result.
                         10: begin
                             jack <= data_out;
                             cmd <= I2CMASTER_STOP;
+`ifdef TOUCH_SENSE_ENABLED
+                            i2c_state <= I2C_TOUCH_SCAN1;
+`else
                             i2c_state <= I2C_LED1;
+`endif // TOUCH_SENSE_ENABLED
                             delay_cnt <= 0;
                         end
                         default: begin
@@ -325,6 +505,76 @@ always_ff @(posedge clk) begin
                     ack_in <= 1'b1;
                     stb <= 1'b1;
                 end
+`ifdef TOUCH_SENSE_ENABLED
+                I2C_TOUCH_SCAN1: begin
+                    i2c_state <= I2C_TOUCH_SCAN2;
+                    i2c_config_pos <= 0;
+                    stb <= 1'b0;
+                end
+                I2C_TOUCH_SCAN2: begin
+                    case (i2c_config_pos)
+                        // Set slave read pointer
+                        0: cmd <= I2CMASTER_START;
+                        1: begin
+                            data_in <= 8'h6E;
+                            cmd <= I2CMASTER_WRITE;
+                        end
+                        // Sensor 0 difference counts
+                        2: begin
+                            if (ack_out == 1'b1) begin
+                                i2c_state <= I2C_LED1;
+                                cmd <= I2CMASTER_STOP;
+                            end else begin
+                                case (nsensor)
+                                    0: data_in <= 8'hBA;
+                                    1: data_in <= 8'hBC;
+                                    2: data_in <= 8'hBE;
+                                    3: data_in <= 8'hC0;
+                                    4: data_in <= 8'hC2;
+                                    5: data_in <= 8'hC4;
+                                    6: data_in <= 8'hC6;
+                                    7: data_in <= 8'hC8;
+                                endcase
+                            end
+                        end
+                        3: cmd <= I2CMASTER_STOP;
+
+                        // Read out the data
+                        4: cmd <= I2CMASTER_START;
+                        5: begin
+                            data_in <= 8'h6F;
+                            cmd <= I2CMASTER_WRITE;
+                        end
+                        6: begin
+                            if (ack_out == 1'b1) begin
+                                i2c_state <= I2C_LED1;
+                                cmd <= I2CMASTER_STOP;
+                            end else begin
+                                cmd <= I2CMASTER_READ;
+                                ack_in <= 1'b1;
+                            end
+                        end
+                        7: begin
+                            case (nsensor)
+                                0: touch0 <= data_out;
+                                1: touch1 <= data_out;
+                                2: touch2 <= data_out;
+                                3: touch3 <= data_out;
+                                // R3.3 hw swaps last four vs R3.2 to improve PCB routing
+                                4: touch7 <= data_out;
+                                5: touch6 <= data_out;
+                                6: touch5 <= data_out;
+                                7: touch4 <= data_out;
+                            endcase
+                            cmd <= I2CMASTER_STOP;
+                            i2c_state <= I2C_LED1;
+                            nsensor <= nsensor + 1;
+                        end
+                    endcase
+                    i2c_config_pos <= i2c_config_pos + 1;
+                    stb <= 1'b1;
+                end
+`endif // TOUCH_SENSE_ENABLED
                 default: begin
                     i2c_state <= I2C_IDLE;
                 end
diff --git a/gateware/eurorack_pmod.sv b/gateware/eurorack_pmod.sv
index 8cdf403..3a240f6 100644
--- a/gateware/eurorack_pmod.sv
+++ b/gateware/eurorack_pmod.sv
@@ -49,6 +49,16 @@ module eurorack_pmod #(
     // Jack detection inputs read constantly over I2C.
     // Logic '1' == jack is inserted. Bit 0 is input 0.
     output [7:0] jack,
+    // Touch sense outputs, one per channel.
+    // If touch sensing is disabled these are just zeroes.
+    output logic [7:0] touch0,
+    output logic [7:0] touch1,
+    output logic [7:0] touch2,
+    output logic [7:0] touch3,
+    output logic [7:0] touch4,
+    output logic [7:0] touch5,
+    output logic [7:0] touch6,
+    output logic [7:0] touch7,
 
     // Signals used for bringup / debug / calibration.
     //
@@ -68,6 +78,18 @@ logic signed [W-1:0] sample_dac1;
 logic signed [W-1:0] sample_dac2;
 logic signed [W-1:0] sample_dac3;
 
+`ifndef TOUCH_SENSE_ENABLED
+assign touch0 = 0;
+assign touch1 = 0;
+assign touch2 = 0;
+assign touch3 = 0;
+assign touch4 = 0;
+assign touch5 = 0;
+assign touch6 = 0;
+assign touch7 = 0;
+`endif
+
+
 // Raw sample calibrator, both for input and output channels.
 // Compensates for DC bias in CODEC, gain differences, resistor
 // tolerances and so on.
@@ -149,6 +171,17 @@ pmod_i2c_master #(
     .led6(force_dac_output == 0 ? cal_out2[W-1:W-8] : force_dac_output[W-1:W-8]),
     .led7(force_dac_output == 0 ? cal_out3[W-1:W-8] : force_dac_output[W-1:W-8]),
 
+`ifdef TOUCH_SENSE_ENABLED
+    .touch0(touch0),
+    .touch1(touch1),
+    .touch2(touch2),
+    .touch3(touch3),
+    .touch4(touch4),
+    .touch5(touch5),
+    .touch6(touch6),
+    .touch7(touch7),
+`endif // TOUCH_SENSE_ENABLED
+
     .jack(jack),
 
     .eeprom_mfg_code(eeprom_mfg),
diff --git a/gateware/mk/common.mk b/gateware/mk/common.mk
index 5873876..0686490 100644
--- a/gateware/mk/common.mk
+++ b/gateware/mk/common.mk
@@ -15,6 +15,7 @@ SRC_COMMON = eurorack_pmod.sv \
 		     cores/pitch_shift.sv \
 		     cores/stereo_echo.sv \
 		     cores/filter.sv \
+		     cores/touch_cv.sv \
 		     cores/util/filter/karlsen_lpf_pipelined.sv \
 		     cores/util/filter/karlsen_lpf.sv \
 		     cores/util/transpose.sv \
diff --git a/gateware/mk/ecp5.mk b/gateware/mk/ecp5.mk
index 78749ac..1fa6ff6 100644
--- a/gateware/mk/ecp5.mk
+++ b/gateware/mk/ecp5.mk
@@ -1,4 +1,4 @@
-DEFINES = "$(ADD_DEFINES) -DECP5 -D$(HW_REV)"
+DEFINES = "$(ADD_DEFINES) -DECP5 -D$(HW_REV) -D$(TOUCH)"
 
 all: $(BUILD)/$(PROJ).bin
 
diff --git a/gateware/mk/ice40.mk b/gateware/mk/ice40.mk
index 8687941..4efba94 100644
--- a/gateware/mk/ice40.mk
+++ b/gateware/mk/ice40.mk
@@ -1,4 +1,4 @@
-DEFINES = "$(ADD_DEFINES) -DICE40 -D$(HW_REV)"
+DEFINES = "$(ADD_DEFINES) -DICE40 -D$(HW_REV) -D$(TOUCH)"
 
 all: $(BUILD)/$(PROJ).bin
 
diff --git a/gateware/scripts/verilator_lint.sh b/gateware/scripts/verilator_lint.sh
index 0f69147..2c63734 100755
--- a/gateware/scripts/verilator_lint.sh
+++ b/gateware/scripts/verilator_lint.sh
@@ -28,6 +28,21 @@ verilator --lint-only -DVERILATOR_LINT_ONLY \
     -Wno-INITIALDLY \
     top.sv
 
+# Lint an entire ICE40 design with touch scanning enabled.
+verilator --lint-only -DVERILATOR_LINT_ONLY \
+    -DICE40 \
+    -DSELECTED_DSP_CORE=touch_cv \
+    -DTOUCH_SENSE_ENABLED \
+    -Iboards/icebreaker \
+    -Ical \
+    -Idrivers \
+    -Iexternal \
+    -Iexternal/no2misc/rtl \
+    -Icores \
+    -Icores/util \
+    -Wno-INITIALDLY \
+    top.sv
+
 # Lint each core which can be selected
 verilator --lint-only -Icores mirror.sv
 verilator --lint-only -Icores bitcrush.sv
@@ -35,6 +50,7 @@ verilator --lint-only -Icores clkdiv.sv
 verilator --lint-only -Icores sampler.sv
 verilator --lint-only -Icores seqswitch.sv
 verilator --lint-only -Icores vca.sv
+verilator --lint-only -Icores touch_cv.sv
 verilator --lint-only -Icores -Icores/util vco.sv
 verilator --lint-only cores/util/filter/karlsen_lpf.sv
 verilator --lint-only cores/util/filter/karlsen_lpf_pipelined.sv
diff --git a/gateware/sim/pmod_i2c_master/tb_pmod_i2c_master.py b/gateware/sim/pmod_i2c_master/tb_pmod_i2c_master.py
index 982f418..16af83e 100644
--- a/gateware/sim/pmod_i2c_master/tb_pmod_i2c_master.py
+++ b/gateware/sim/pmod_i2c_master/tb_pmod_i2c_master.py
@@ -27,7 +27,7 @@ async def test_i2cinit_00(dut):
 
     dut.rst.value = 0
 
-    dut.i2c_state.value = 5 # Jump to I2C_INIT_CODEC1
+    dut.i2c_state.value = 7 # Jump to I2C_INIT_CODEC1
 
     await RisingEdge(dut.sda_oe)
 
diff --git a/gateware/top.sv b/gateware/top.sv
index 56a6e1e..8a81131 100644
--- a/gateware/top.sv
+++ b/gateware/top.sv
@@ -53,6 +53,14 @@ logic [7:0]  eeprom_mfg;
 logic [7:0]  eeprom_dev;
 logic [31:0] eeprom_serial;
 logic [7:0]  jack;
+logic [7:0]  touch0;
+logic [7:0]  touch1;
+logic [7:0]  touch2;
+logic [7:0]  touch3;
+logic [7:0]  touch4;
+logic [7:0]  touch5;
+logic [7:0]  touch6;
+logic [7:0]  touch7;
 
 // Tristated I2C signals must be broken out at the top level as
 // ECP5 flow does not support tristate signals in nested modules.
@@ -91,18 +99,28 @@ sysmgr sysmgr_instance (
 `SELECTED_DSP_CORE #(
     .W(W)
 ) dsp_core_instance (
-    .rst         (rst),
-    .clk         (clk_256fs),
-    .sample_clk  (clk_fs),
-    .sample_in0  (in0),
-    .sample_in1  (in1),
-    .sample_in2  (in2),
-    .sample_in3  (in3),
-    .sample_out0 (out0),
-    .sample_out1 (out1),
-    .sample_out2 (out2),
-    .sample_out3 (out3),
-    .jack        (jack)
+      .rst         (rst)
+    , .clk         (clk_256fs)
+    , .sample_clk  (clk_fs)
+    , .sample_in0  (in0)
+    , .sample_in1  (in1)
+    , .sample_in2  (in2)
+    , .sample_in3  (in3)
+    , .sample_out0 (out0)
+    , .sample_out1 (out1)
+    , .sample_out2 (out2)
+    , .sample_out3 (out3)
+    , .jack        (jack)
+`ifdef TOUCH_SENSE_ENABLED
+    , .touch0      (touch0)
+    , .touch1      (touch1)
+    , .touch2      (touch2)
+    , .touch3      (touch3)
+    , .touch4      (touch4)
+    , .touch5      (touch5)
+    , .touch6      (touch6)
+    , .touch7      (touch7)
+`endif
 );
 
 `ifdef ECP5
@@ -156,6 +174,14 @@ eurorack_pmod #(
     .cal_out2     (out2),
     .cal_out3     (out3),
     .jack         (jack),
+    .touch0       (touch0),
+    .touch1       (touch1),
+    .touch2       (touch2),
+    .touch3       (touch3),
+    .touch4       (touch4),
+    .touch5       (touch5),
+    .touch6       (touch6),
+    .touch7       (touch7),
     .eeprom_mfg   (eeprom_mfg),
     .eeprom_dev   (eeprom_dev),
     .eeprom_serial(eeprom_serial),
@@ -187,7 +213,15 @@ debug_uart #(
     .eeprom_mfg(eeprom_mfg),
     .eeprom_dev(eeprom_dev),
     .eeprom_serial(eeprom_serial),
-    .jack(jack)
+    .jack(jack),
+    .touch0(touch0),
+    .touch1(touch1),
+    .touch2(touch2),
+    .touch3(touch3),
+    .touch4(touch4),
+    .touch5(touch5),
+    .touch6(touch6),
+    .touch7(touch7)
 );
 
 `ifdef COCOTB_SIM