{
  "family": "Sense",
  "name": "T.C",
  "rev": "a",
  "tile_id": 18,
  "json_version": "0.7",
  "updated_at": "2026-05-01T12:41:02.767Z",
  "headline": "capacitive touch",
  "description": "Built around the Azoteq IQS323 ProxFusion controller, the Sense.T.C tile's entire top surface functions as a capacitive touch sensor, while a second input channel is available via one of the pads.\n\nThe IQS323 provides both proximity and touch detection with all signal processing on-chip, exposing processed results over I2C. Each channel reports 16-bit filtered counts, a 16-bit long-term average baseline, and binary proximity/touch states with configurable thresholds, debounce, and hysteresis. When configured as a slider across multiple tiles, the controller provides a 16-bit position output with on-chip gesture recognition including tap, swipe, flick, and hold.\n\nReport rates are configurable per power mode, from every 16ms in normal self-capacitive mode (125µA average current consumption) up to every 160ms in 4 µA ultra-low-power mode (160 ms). In addition, event-driven reporting can leave the host free between changes in state.",
  "application_notes": [
    {
      "sort": 0,
      "details": "The entire top surface of the tile connected to the C1 sensor input with a 470-ohm series resistor.",
      "heading": "Sensing Surface",
      "image_url": ""
    },
    {
      "sort": 1,
      "details": "The C0 input on the IQS323 is directly connected to pad 8 of the tile, allowing for user configuration of a secondary capacitive touch input.",
      "heading": "Secondary Input",
      "image_url": ""
    }
  ],
  "package": {
    "pads": 10,
    "type": "T44",
    "size_x": 4000,
    "size_y": 4000,
    "size_z": 0
  },
  "power": [
    {
      "max": 3.5,
      "min": 1.71,
      "type": "system",
      "notes": "",
      "gnd_pad": [
        "1"
      ],
      "function": "",
      "direction": "input",
      "is_required": true,
      "max_current": "",
      "positive_pad": [
        "10"
      ]
    }
  ],
  "components": [
    {
      "url": "https://www.azoteq.com/product/iqs323/",
      "part": "IQS323",
      "datasheet": "https://mosaic-component-datasheets.s3.eu-north-1.amazonaws.com/18/Azoteq-IQ323.pdf",
      "manufacturer": "Azoteq"
    }
  ],
  "pads": [
    {
      "pad": "1",
      "geometry": {
        "size_x": 1000,
        "size_y": 400,
        "center_x": -1500,
        "center_y": 1600
      },
      "functions": [
        {
          "note": "",
          "type": "power",
          "function": "GND",
          "direction": "input"
        }
      ]
    },
    {
      "pad": "2",
      "geometry": {
        "size_x": 800,
        "size_y": 400,
        "center_x": -1600,
        "center_y": 800
      },
      "functions": []
    },
    {
      "pad": "3",
      "geometry": {
        "size_x": 800,
        "size_y": 400,
        "center_x": -1600,
        "center_y": 0
      },
      "functions": [
        {
          "note": "configurable interrupt output (with an internal 4.7k pull up on the open-drain)",
          "type": "digital",
          "function": "RDY",
          "direction": ""
        }
      ]
    },
    {
      "pad": "4",
      "geometry": {
        "size_x": 800,
        "size_y": 400,
        "center_x": -1600,
        "center_y": -800
      },
      "functions": [
        {
          "note": "",
          "type": "digital",
          "function": "I2C.CLK",
          "direction": "bidirectional",
          "interface": "I2C"
        }
      ]
    },
    {
      "pad": "5",
      "geometry": {
        "size_x": 800,
        "size_y": 400,
        "center_x": -1600,
        "center_y": -1600
      },
      "functions": [
        {
          "note": "",
          "type": "digital",
          "function": "I2C.DAT",
          "direction": "bidirectional",
          "interface": "I2C"
        }
      ]
    },
    {
      "pad": "6",
      "geometry": {
        "size_x": 800,
        "size_y": 400,
        "center_x": 1600,
        "center_y": -1600
      },
      "functions": []
    },
    {
      "pad": "7",
      "geometry": {
        "size_x": 800,
        "size_y": 400,
        "center_x": 1600,
        "center_y": -800
      },
      "functions": []
    },
    {
      "pad": "8",
      "geometry": {
        "size_x": 800,
        "size_y": 400,
        "center_x": 1600,
        "center_y": 0
      },
      "functions": [
        {
          "note": "additional external cap-touch input",
          "type": "analog",
          "function": "C0",
          "direction": "input"
        }
      ]
    },
    {
      "pad": "9",
      "geometry": {
        "size_x": 800,
        "size_y": 400,
        "center_x": 1600,
        "center_y": 800
      },
      "functions": []
    },
    {
      "pad": "10",
      "geometry": {
        "size_x": 800,
        "size_y": 400,
        "center_x": 1600,
        "center_y": 1600
      },
      "functions": [
        {
          "note": "1.71-3.5V",
          "type": "power",
          "function": "V+",
          "direction": "input"
        }
      ]
    }
  ],
  "interfaces": [
    {
      "name": "I2C",
      "type": "I2C",
      "parameters": {
        "modes": [
          "slave"
        ],
        "addresses": [
          {
            "address": "0x44"
          }
        ],
        "max_clock_speed": "1MHz"
      },
      "pad_assignments": [
        {
          "pad": "4",
          "role": "bus",
          "function": "I2C.CLK"
        },
        {
          "pad": "5",
          "role": "bus",
          "function": "I2C.DAT"
        }
      ]
    }
  ],
  "twin": {
    "score": 1,
    "source": "// Digital twin for Sense.T.C — Azoteq IQS323 ProxFusion capacitive touch.\n//\n// A counts-based charge-transfer cap sensor (NOT a femtofarad CDC — it reports\n// filtered conversion counts vs a long-term-average baseline, never absolute\n// capacitance). This is a DOUBLE-SIDED tile: the IC + passives are on the\n// bottom; the ENTIRE TOP SURFACE is the self-cap electrode for channel CH1 (via\n// a 470Ω series R), and CH0 is a second external electrode brought out to pad 8.\n// CH2 exists in the chip but is unpopulated on this tile.\n//\n// Pad map (Sense-T-C-a.json): GND (1), RDY/MCLR (3, open-drain output, on-tile\n// 4.7k pull-up + 100nF), I2C (4/5), C0 external electrode (8), V+ (10).\n//\n// Modeled: the two touch electrodes are driven by the controls; counts/LTA/delta\n// and the System-Status bits follow from them, matching the driver's register\n// layout (canonical). The absolute count magnitudes are modeled (inferred); a\n// real chip's working point comes from ATI. Slider/gesture are not usable on a\n// single-surface tile, so those reads are honest no-ops (hallucinated).\nimport type { TileSim } from '../tileSim';\n\nconst I2C_ADDR = 0x44; // order code 001 (per tile JSON)\n\n// Channel indices (driver SENSE_T_C_CH*): 0 = external (pad 8), 1 = surface, 2 = unpopulated.\nconst CH_EXTERNAL = 0;\nconst CH_SURFACE = 1;\n\n// Modeled counts: a flat LTA baseline; touch/prox raise counts above it.\nconst LTA_BASE = 1000;\nconst TOUCH_DELTA = 220;\nconst PROX_DELTA = 60;\n\n// Power-mode quiescent current (datasheet §3.4, self-cap 3ch, 3.3V), µA.\nconst I_NORMAL_UA = 125;\nconst I_LOW_UA = 38;\nconst I_ULP_UA = 4;\nconst I_HALT_UA = 2;\n\n// Power-mode codes (sense_t_c_power_mode_t)\nconst PM_NORMAL = 0;\nconst PM_LOW = 1;\nconst PM_ULP = 2;\nconst PM_HALT = 3;\n\n// System-status bits (IQS323_STATUS_*): per-channel prox = bit(8+ch*2), touch = bit(9+ch*2).\nconst ST_PROX_EVENT = 1 << 0;\nconst ST_TOUCH_EVENT = 1 << 1;\nconst chProxBit = (ch: number) => 1 << (8 + ch * 2);\nconst chTouchBit = (ch: number) => 1 << (9 + ch * 2);\n\nconst SLIDER_INVALID = 0xffff; // no slider configurable on a single-surface tile\n\ninterface State {\n  // ── electrodes (driven by controls) ──\n  touch_surface: number; // CH1 — whole top face\n  touch_external: number; // CH0 — pad 8\n  prox_surface: number; // near-but-not-touch on the surface\n  prox_external: number;\n\n  // ── config (tracked) ──\n  power_mode: number;\n  comm_mode: number; // 0 stream, 1 event\n  events_mask: number;\n  prox_thr: number; // shared threshold model (per-channel in HW)\n  touch_thr: number;\n  np_rate_ms: number;\n  lp_rate_ms: number;\n  ulp_rate_ms: number;\n  halt_rate_ms: number;\n  power_timeout_ms: number;\n\n  [field: string]: number;\n}\n\nconst pick = (args: number[], i: number, cur: number) =>\n  args.length > i && Number.isFinite(args[i]) ? args[i] : cur;\nconst clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));\n\n// Per-channel touch/prox state from the modeled electrodes (CH2 is unpopulated).\nfunction touchOf(s: State, ch: number): number {\n  if (ch === CH_SURFACE) return s.touch_surface ? 1 : 0;\n  if (ch === CH_EXTERNAL) return s.touch_external ? 1 : 0;\n  return 0;\n}\nfunction proxOf(s: State, ch: number): number {\n  if (touchOf(s, ch)) return 1; // touch implies prox\n  if (ch === CH_SURFACE) return s.prox_surface ? 1 : 0;\n  if (ch === CH_EXTERNAL) return s.prox_external ? 1 : 0;\n  return 0;\n}\nfunction deltaOf(s: State, ch: number): number {\n  if (touchOf(s, ch)) return TOUCH_DELTA;\n  if (proxOf(s, ch)) return PROX_DELTA;\n  return 0;\n}\nconst countsOf = (s: State, ch: number) => LTA_BASE + deltaOf(s, ch);\n\n// System Status word the driver reads from 0x10.\nfunction statusWord(s: State): number {\n  let st = 0;\n  for (const ch of [CH_EXTERNAL, CH_SURFACE]) {\n    if (proxOf(s, ch)) st |= chProxBit(ch);\n    if (touchOf(s, ch)) st |= chTouchBit(ch);\n  }\n  if (proxOf(s, CH_EXTERNAL) || proxOf(s, CH_SURFACE)) st |= ST_PROX_EVENT;\n  if (touchOf(s, CH_EXTERNAL) || touchOf(s, CH_SURFACE)) st |= ST_TOUCH_EVENT;\n  return st;\n}\n\nconst anyTouched = (s: State) => (touchOf(s, CH_EXTERNAL) || touchOf(s, CH_SURFACE) ? 1 : 0);\n\nfunction modeCurrentUa(mode: number): number {\n  switch (mode) {\n    case PM_LOW:\n      return I_LOW_UA;\n    case PM_ULP:\n      return I_ULP_UA;\n    case PM_HALT:\n      return I_HALT_UA;\n    default:\n      return I_NORMAL_UA; // NORMAL / AUTO / AUTO_NO_ULP active\n  }\n}\n\nconst sim: TileSim<State> = {\n  tile: 'Sense.T.C',\n\n  defaultState: {\n    touch_surface: 0,\n    touch_external: 0,\n    prox_surface: 0,\n    prox_external: 0,\n\n    power_mode: PM_NORMAL,\n    comm_mode: 1, // event mode (chip default)\n    events_mask: ST_PROX_EVENT | ST_TOUCH_EVENT,\n    prox_thr: 20,\n    touch_thr: 40,\n    np_rate_ms: 16,\n    lp_rate_ms: 60,\n    ulp_rate_ms: 160,\n    halt_rate_ms: 3000,\n    power_timeout_ms: 2000,\n  },\n\n  controls: [\n    { type: 'toggle', field: 'touch_surface', label: 'Touch top surface (C1)' },\n    { type: 'toggle', field: 'touch_external', label: 'Touch C0 (pad 8)' },\n    { type: 'toggle', field: 'prox_surface', label: 'Approach surface (prox)' },\n    {\n      type: 'slider',\n      field: 'power_mode',\n      label: 'Power mode (0 NP/1 LP/2 ULP/3 Halt)',\n      min: 0,\n      max: 3,\n      step: 1,\n    },\n  ],\n\n  hostCalls: {\n    // ── lifecycle ──\n    tile_sense_t_c_find: () => ({ scalar: I2C_ADDR }),\n    tile_sense_t_c_init: () => ({ scalar: 0, nextState: { power_mode: PM_NORMAL } }),\n    tile_sense_t_c_process: ({ state }) => ({ scalar: statusWord(state) }),\n    tile_sense_t_c_on_event: () => ({ nextState: {} }),\n    tile_sense_t_c_sleep: () => ({ nextState: { power_mode: PM_HALT } }),\n    tile_sense_t_c_wake: () => ({ nextState: { power_mode: PM_NORMAL } }),\n\n    // ── status / data ──\n    tile_sense_t_c_get_status: ({ state }) => ({ scalar: statusWord(state) }),\n    tile_sense_t_c_get_gestures: () => ({ scalar: 0 }), // no gesture surface on this tile\n    tile_sense_t_c_is_touched: ({ state, args }) => ({ scalar: touchOf(state, pick(args, 0, 0)) }),\n    tile_sense_t_c_is_prox: ({ state, args }) => ({ scalar: proxOf(state, pick(args, 0, 0)) }),\n    tile_sense_t_c_is_touched_any: ({ state }) => ({ scalar: anyTouched(state) }),\n    tile_sense_t_c_get_counts: ({ state, args }) => ({ scalar: countsOf(state, pick(args, 0, 0)) }),\n    tile_sense_t_c_get_lta: () => ({ scalar: LTA_BASE }),\n    tile_sense_t_c_get_delta: ({ state, args }) => ({ scalar: deltaOf(state, pick(args, 0, 0)) }),\n    tile_sense_t_c_get_slider: () => ({ scalar: SLIDER_INVALID }),\n    tile_sense_t_c_read_slider_pct: () => ({ array: [0, 0] }), // [ok, pct] — no slider\n    tile_sense_t_c_wait_for_touch: ({ state }) => ({ scalar: anyTouched(state) }),\n    tile_sense_t_c_wait_for_gesture: () => ({ scalar: 0 }),\n\n    // ── config ──\n    tile_sense_t_c_set_thresholds: ({ args }) => ({\n      nextState: {\n        prox_thr: clamp(pick(args, 1, 20), 0, 255),\n        touch_thr: clamp(pick(args, 2, 40), 0, 255),\n      },\n    }),\n    tile_sense_t_c_set_power_mode: ({ args }) => ({\n      nextState: { power_mode: clamp(pick(args, 0, PM_NORMAL), 0, 5) },\n    }),\n    tile_sense_t_c_enable_events: ({ args }) => ({ nextState: { events_mask: pick(args, 0, 0) } }),\n    tile_sense_t_c_ati: () => ({ nextState: {} }),\n    tile_sense_t_c_set_ati_setup: () => ({ nextState: {} }),\n    tile_sense_t_c_set_counts_filter: () => ({ nextState: {} }),\n    tile_sense_t_c_set_conversion_freq: () => ({ nextState: {} }),\n    tile_sense_t_c_set_channel_mode: () => ({ nextState: {} }),\n    tile_sense_t_c_set_comm_mode: ({ args }) => ({\n      nextState: { comm_mode: pick(args, 0, 1) ? 1 : 0 },\n    }),\n\n    // ── low-level ──\n    tile_sense_t_c_read_reg: () => ({ scalar: 0 }),\n    tile_sense_t_c_write_reg: () => ({ nextState: {} }),\n\n    // ── v1.3 additions ──\n    tile_sense_t_c_reseed: () => ({ nextState: {} }), // re-snaps LTA to counts; baseline is flat here\n    tile_sense_t_c_set_report_rate: ({ args }) => {\n      const mode = pick(args, 0, PM_NORMAL);\n      const ms = clamp(pick(args, 1, 16), 0, 3000);\n      const field =\n        mode === PM_LOW\n          ? 'lp_rate_ms'\n          : mode === PM_ULP\n            ? 'ulp_rate_ms'\n            : mode === PM_HALT\n              ? 'halt_rate_ms'\n              : 'np_rate_ms';\n      return { nextState: { [field]: ms } };\n    },\n    tile_sense_t_c_set_power_timeout: ({ args }) => ({\n      nextState: { power_timeout_ms: clamp(pick(args, 0, 2000), 0, 65000) },\n    }),\n    // Nominal tuned working point (divider 31, value ~934) — matches Azoteq EV-kit.\n    tile_sense_t_c_get_compensation: () => ({ scalar: (31 << 11) | 934 }),\n    tile_sense_t_c_set_compensation: () => ({ nextState: {} }),\n  },\n\n  provenance: {\n    tile_sense_t_c_find: 'canonical', // I2C 0x44\n    tile_sense_t_c_get_status: 'canonical', // System Status bit layout\n    tile_sense_t_c_is_touched: 'canonical',\n    tile_sense_t_c_is_prox: 'canonical',\n    tile_sense_t_c_is_touched_any: 'canonical',\n    tile_sense_t_c_get_lta: 'canonical', // LTA register\n    tile_sense_t_c_set_power_mode: 'canonical', // SYSTEM_CONTROL[6:4]\n    tile_sense_t_c_set_comm_mode: 'canonical', // SYSTEM_CONTROL bit7 (1=event)\n    tile_sense_t_c_set_thresholds: 'canonical', // Prox/Touch Settings byte layout\n    tile_sense_t_c_reseed: 'canonical', // SYSTEM_CONTROL.RESEED bit3\n    tile_sense_t_c_set_report_rate: 'canonical', // 0xC1-0xC4, ms\n    tile_sense_t_c_set_power_timeout: 'canonical', // 0xC5, ms\n    tile_sense_t_c_get_compensation: 'canonical', // 0x39+ encoding\n    tile_sense_t_c_set_compensation: 'canonical',\n    // inferred — modeled magnitudes / behavioral stubs\n    tile_sense_t_c_get_counts: 'inferred', // modeled count magnitude\n    tile_sense_t_c_get_delta: 'inferred',\n    tile_sense_t_c_process: 'inferred',\n    tile_sense_t_c_ati: 'inferred',\n    // hallucinated — not usable on this single-surface tile\n    tile_sense_t_c_get_slider: 'hallucinated',\n    tile_sense_t_c_read_slider_pct: 'hallucinated',\n    tile_sense_t_c_get_gestures: 'hallucinated',\n    tile_sense_t_c_wait_for_gesture: 'hallucinated',\n    tile_sense_t_c_read_reg: 'hallucinated',\n    power: 'canonical', // datasheet §3.4 per-mode current\n  },\n\n  // RDY (pad 3) is the one tile-driven digital output: open-drain, asserted LOW\n  // on an enabled event in event mode. Shown 1 = asserted for clarity. C0 (pad 8)\n  // and the C1 top surface are capacitance sense INPUTS, not driven outputs.\n  padOutputs(state) {\n    const event = (statusWord(state) & state.events_mask) !== 0;\n    const asserted = state.comm_mode === 1 && event ? 1 : 0;\n    return { RDY: asserted };\n  },\n\n  // Electrical: a pure load on V+ (pad 10) / GND (pad 1). Draw depends on the\n  // power mode (datasheet §3.4); no rail is sourced. ULP/Halt collapse to µA.\n  power(state) {\n    const ua = modeCurrentUa(state.power_mode);\n    return {\n      draw_ua: ua,\n      rails: [\n        {\n          name: 'V+',\n          role: 'supply',\n          v_mv: 3300,\n          i_ua: ua,\n          pads: ['10'],\n          note: `IQS323 (${['NP', 'LP', 'ULP', 'Halt'][state.power_mode] ?? 'NP'})`,\n        },\n      ],\n    };\n  },\n};\n\nexport default sim;\n",
    "status": "validated",
    "updated_at": "2026-06-22T12:44:21.678Z"
  },
  "config": {
    "dataReady": {
      "kind": "boolean",
      "group": "Sidebands",
      "label": "Use RDY (data-ready) line",
      "binding": {
        "pad": "3",
        "kind": "output"
      },
      "default": false,
      "options": [
        {
          "value": false,
          "firmware_contract": [
            {
              "via": "i2c",
              "type": "register",
              "value": "disabled",
              "register": "RDY_CONFIG"
            }
          ]
        },
        {
          "value": true,
          "netlist": {
            "expects": [
              {
                "to": {
                  "kind": "matchFunction",
                  "function": "GPIO",
                  "capabilities": [
                    "EXTI"
                  ]
                },
                "tag": "dataReady.attached",
                "from": {
                  "pad": "3",
                  "kind": "tile"
                },
                "role": "interrupt"
              }
            ]
          },
          "firmware_contract": [
            {
              "via": "i2c",
              "type": "register",
              "value": "enabled",
              "register": "RDY_CONFIG"
            }
          ]
        }
      ]
    }
  }
}