abse/smartwatch-v1-pcb

Routed bare ESP32-S3 smartwatch with SDR PSRAM, touch AMOLED, Qi receiver and compact/exploded 39 × 50.5 × 14.5 mm provisional assembly.

Version
0.2.0
License
unset
Stars
0

esp32-s3-board.tsx

import React from 'react'
import type { AutorouterConfig } from '@tscircuit/props'
import { pinNets as historicalPins, resistors as historicalResistors, capacitors as historicalCapacitors } from './historical-module.circuit'
import historicalPlacement from './placement.json'
import { ESP32_S3 } from './components/esp32/ESP32_S3'
import { APS6404L_3SQR_SN } from './imports/APS6404L_3SQR_SN'
import { W25Q128JVSIQ } from './imports/W25Q128JVSIQ'
import { Q22FA12800332 } from './imports/Q22FA12800332'
import { A_2450AT18A100E } from './imports/A_2450AT18A100E'
import { BMI270 } from './imports/BMI270'
import { LIS2MDLTR } from './imports/LIS2MDLTR'
import { VEML6030 } from './imports/VEML6030'
import { BQ24074RGTR } from './imports/BQ24074RGTR'
import { TPS63802DLAR } from './imports/TPS63802DLAR'
import { MAX17048G_T10 } from './imports/MAX17048G_T10'
import { DRV2605LDGSR } from './imports/DRV2605LDGSR'
import { AP2112K_3_3TRG1 } from './imports/AP2112K_3_3TRG1'
import { TLV75801PDBVR } from './imports/TLV75801PDBVR'
import { TPS7A0218PDBVR } from './imports/TPS7A0218PDBVR'
import { ZSPD4003LG } from './imports/ZSPD4003LG'
import { BSS138 } from './imports/BSS138'
import { TLV7041DCKR } from './imports/TLV7041DCKR'
import { B3U_3000P } from './components/esp32/B3U_3000P'
import { LESD5Z5_0CT1G } from './imports/LESD5Z5_0CT1G'
import { StandardJstUartUpward } from './imports/StandardJstUartUpward'
import { SLO201610TR47MTT } from './components/SLO201610TR47MTT'
import { WirePads, DockPads, ProbePad } from './components/BoardPads'
import { BQ51013BRHLR } from './components/esp32/BQ51013BRHLR'
import { SS14 } from './components/esp32/SS14'
import { KMR221GLFS } from './components/esp32/KMR221GLFS'

// RGTT and RGTR share TI's RGT package, die and pinout; only reel quantity
// differs. C2861252 is currently orderable, whereas C54313 has no orderable stock.
const BQ24074RGTT=(props:any)=><BQ24074RGTR {...props} manufacturerPartNumber="BQ24074RGTT" supplierPartNumbers={{jlcpcb:['C2861252']}}/>

// This is reconstructed source, not replay of the unavailable accepted bare-MCU board.
// Exact native extraction, physical routing and all-layer audits are required.
export const pinNets: Record<string, Record<number, string>> = structuredClone(historicalPins)
for (const ref of ['R46','R32','R33','M1','U13','U16','Q1','Q2','J1','J3','J5','R26','R28','R29','C27','C28','C34','C35','TH1']) delete pinNets[ref]
const rename: Record<string,string> = {
  LCD_VCC:'OLED_VDD', LCD_DC_M:'OLED_D1', LCD_MOSI_M:'OLED_D0', LCD_CLK_M:'OLED_CLK',
  LCD_CS_M:'OLED_CS', LCD_RST_M:'OLED_RST', BL_PWM:'OLED_BOOST_EN', TP_SCL:'SCL', TP_SDA:'SDA',
}
for (const pins of Object.values(pinNets)) for (const [p,n] of Object.entries(pins)) pins[Number(p)]=rename[n]??n
pinNets.U1={1:'RF_CHIP',2:'VDD_RF',3:'VDD_RF',4:'MCU_EN',5:'BOOT',6:'IMU_INT',7:'PPG_INT',9:'KEY',10:'CHARGE_INHIBIT',11:'SCL',12:'SDA',13:'TP_RST',14:'TP_INT',15:'OLED_CS',16:'OLED_D0',17:'OLED_CLK',18:'OLED_D1',19:'OLED_RST',20:'V3V3',23:'OLED_D2',24:'DISP_EN',27:'OLED_D3',28:'RAM_CS',29:'V3V3',30:'MEM_D3',31:'MEM_D2',32:'FLASH_CS',33:'MEM_CLK',34:'MEM_D1',35:'MEM_D0',38:'HAPTIC_EN',40:'GAUGE_INT',41:'CHG_STATUS',42:'PGOOD',43:'MAG_INT',44:'ALS_INT',46:'V3V3',47:'OLED_TE',48:'OLED_BOOST_EN',49:'UART_TX',50:'UART_RX',51:'STRAP45',52:'STRAP46',53:'XTAL_N',54:'XTAL_P_MCU',55:'V3V3',56:'V3V3',57:'GND'}
// GPIO1 is an RTC-capable wake source for BMI270 lift-to-wake firmware.
pinNets.U15={1:'FLASH_CS',2:'MEM_D1',3:'MEM_D2',4:'GND',5:'MEM_D0',6:'MEM_CLK',7:'MEM_D3',8:'V3V3'}
pinNets.U19={1:'RAM_CS',2:'MEM_D1',3:'MEM_D2',4:'GND',5:'MEM_D0',6:'MEM_CLK',7:'MEM_D3',8:'V3V3'}
pinNets.X1={1:'XTAL_P',2:'GND',3:'XTAL_N',4:'GND'}
pinNets.A1={1:'RF_ANT'} // Terminal 2 is the mechanical NC end; never connect it to ground.
pinNets.U10={1:'VSYS',2:'GND',3:'VSYS',5:'V3V3'}
pinNets.U12={1:'VSYS',2:'GND',3:'DISP_EN',4:'FB_LCD',5:'OLED_VDD'}
pinNets.J1={1:'OLED_D3',2:'OLED_D2',3:'OLED_D0',4:'OLED_RST',5:'OLED_CLK',6:'OLED_D1',7:'OLED_CS',8:'GND',9:'OLED_TE',10:'OLED_VDD',11:'OLED_BOOST_EN',13:'V3V3',14:'TP_INT',15:'SCL',16:'SDA',17:'TP_RST',19:'GND',20:'GND',21:'GND'}
pinNets.J3={1:'BAT',2:'GND'}
pinNets.J5={1:'UART_RX',2:'GND',3:'UART_CABLE_TX',4:'GND',5:'GND'}
pinNets.SW1={1:'KEY',2:'GND'}
pinNets.SW2={1:'BOOT',2:'GND',3:'BOOT',4:'GND',5:'GND'}
pinNets.SW3={1:'MCU_EN',2:'GND',3:'MCU_EN',4:'GND',5:'GND'}
pinNets.J7={1:'NTC',2:'GND'}
pinNets.J6={1:'QI_COIL1',2:'QI_AC2'}
pinNets.J8={1:'QI_TS',2:'GND'}
pinNets.U20={1:'GND',2:'QI_AC1',3:'QI_BOOT1',4:'QI_5V',5:'QI_CLAMP1',6:'QI_COM1',9:'GND',10:'GND',11:'GND',12:'QI_ILIM',13:'QI_TS',14:'QI_FOD',15:'QI_COM2',16:'QI_CLAMP2',17:'QI_BOOT2',18:'QI_RECT',19:'QI_AC2',20:'GND',21:'GND'}
pinNets.D2={1:'INPUT_5V',2:'DOCK_5V'}
pinNets.D3={1:'INPUT_5V',2:'QI_5V'}
pinNets.U6[13]='INPUT_5V'

export const resistors:any[]=structuredClone(historicalResistors).filter(p=>!['R26','R28','R29','TH1','R32','R33'].includes(p.name))
export const capacitors:any[]=structuredClone(historicalCapacitors).filter(p=>!['C27','C28','C34','C35'].includes(p.name))
for(const p of [...resistors,...capacitors]){p.a=rename[p.a]??p.a;p.b=rename[p.b]??p.b}
for(const name of ['R24','R25']) Object.assign(resistors.find(p=>p.name===name),{value:'47k',part:'C25792'})
Object.assign(resistors.find(p=>p.name==='R36'),{a:'V3V3',b:'KEY'})
const addR=(name:string,value:string,a:string,b:string,part?:string,footprint='0402')=>resistors.push({name,value,a,b,part,footprint})
const addC=(name:string,value:string,a:string,b='GND',part?:string,footprint='0402')=>capacitors.push({name,value,a,b,part,footprint})
addR('R45','499','UART_TX','UART_CABLE_TX')
addR('R47','10k','V3V3','MCU_EN','C25744');addR('R48','10k','V3V3','BOOT','C25744')
addR('R49','10k','STRAP45','GND','C25744');addR('R50','10k','STRAP46','GND','C25744')
addR('R51','10k','V3V3','FLASH_CS','C25744');addR('R52','10k','V3V3','RAM_CS','C25744')

addR('R56','1k','QI_ILIM','QI_FOD','C11702');addR('R57','200','QI_FOD','GND')
// Qi ILIM initial total 1200 ohms => 314/1200 = 262 mA. FOD requires measured calibration.
for(const [name,net] of [['C38','VDD_RF'],['C39','VDD_RF'],['C40','V3V3'],['C41','V3V3'],['C42','V3V3'],['C43','V3V3'],['C44','V3V3'],['C45','V3V3']]) addC(name,'100nF',net,'GND','C1525')
addC('C46','10uF','V3V3','GND','C19702','0603');addC('C47','1uF','MCU_EN','GND','C52923')
addC('C48','18pF','XTAL_P');addC('C49','18pF','XTAL_N')
addC('C50','1.5pF','RF_CHIP', 'GND','C88913','0201');addC('C51','1.5pF','RF_FEED','GND','C88913','0201')
addC('C52','1.5pF','RF_ANT','GND','C88913','0201')
addC('C53','220nF','QI_COIL1','QI_AC1',undefined,'0603');addC('C54','2.2nF','QI_AC1','QI_AC2',undefined,'0603')
addC('C55','10nF','QI_BOOT1','QI_AC1');addC('C56','10nF','QI_BOOT2','QI_AC2')
addC('C57','470nF','QI_CLAMP1','QI_AC1');addC('C58','470nF','QI_CLAMP2','QI_AC2')
addC('C59','22nF','QI_COM1','QI_AC1');addC('C60','22nF','QI_COM2','QI_AC2')
addC('C61','10uF','QI_RECT','GND',undefined,'0603');addC('C62','1uF','QI_5V','GND','C52923')
addC('C71','100nF','OLED_VDD','GND','C1525');addC('C72','4.7uF','V3V3','GND','C23733')
export const inductors:any[]=[{name:'L2',value:'2.7nH',a:'RF_CHIP',b:'RF_FEED',part:'C98043',footprint:'0201'},{name:'L3',value:'24nH',a:'XTAL_P_MCU',b:'XTAL_P',footprint:'0402'},{name:'L4',value:'0nH',a:'RF_FEED',b:'RF_ANT',footprint:'0201',isZeroOhm:true},{name:'L5',value:'0nH',a:'V3V3',b:'VDD_RF',footprint:'0402',isZeroOhm:true}]
// Parametric catalogue matches were independently checked against exact JLC records.
// Text-search results containing unrelated MPNs are deliberately rejected.
const selected:Record<string,any>={
  R45:{part:'C4125',mpn:'0402WGF4990TCE'},R57:{part:'C25087',mpn:'0402WGF2000TCE'},
  C48:{part:'C307443',mpn:'CL05C180JB5NNNC'},C49:{part:'C307443',mpn:'CL05C180JB5NNNC'},
  C53:{part:'C464977',mpn:'GRM31C5C1H224JE02L',footprint:'1206'},
  C54:{part:'C2987940',mpn:'GRM1555C1H222GA01D',footprint:'0402'},
  C55:{part:'C15195',mpn:'CL05B103KB5NNNC'},C56:{part:'C15195',mpn:'CL05B103KB5NNNC'},
  C57:{part:'C513577',mpn:'CC0603KRX7R9BB474',footprint:'0603'},C58:{part:'C513577',mpn:'CC0603KRX7R9BB474',footprint:'0603'},
  C59:{part:'C107026',mpn:'CC0402KRX7R9BB223'},C60:{part:'C107026',mpn:'CC0402KRX7R9BB223'},
  C61:{part:'C440198',mpn:'GRM21BR61H106KE43L',footprint:'0805'},
  L3:{part:'C74301',mpn:'SDCL1005C24NJTDF'},
  L4:{part:'C473473',mpn:'0201WMF0000TEE'},L5:{part:'C17168',mpn:'0402WGF0000TCE'},
}
capacitors.find(p=>p.name==='C11').a='INPUT_5V' // Local charger input bypass works for either source.
for(const p of [...resistors,...capacitors,...inductors]){
  Object.assign(p,selected[p.name]??{});pinNets[p.name]={1:p.a,2:p.b}
}

for(const [i,n] of ['GND','V3V3','VSYS','UART_TX','UART_RX','MCU_EN'].entries())pinNets[`TP${i+1}`]={1:n}

export const placement:Record<string,any>=structuredClone(historicalPlacement)
for(const p of Object.values(placement))p.layer='top'
const place=(name:string,x:number,y:number,layer='top',rot=0)=>placement[name]={pcbX:x,pcbY:y,layer,pcbRotation:rot}
place('U1',-3,3);place('U15',7,5.6);place('U19',7,-2.9);place('X1',-3,9.2);place('A1',-13.7,14.5)
place('U20',-4.5,-5.5);place('U8',-14,-6);place('U17',11.5,4.2);place('U18',14,4.2)
place('U2',9.4,13.2);place('U3',5.5,16.6);place('U4',13.9,16.5)
place('U14',-14.25,.153,'bottom',90);place('J1',2,-16.35,'bottom');place('J3',-14,-12.6,'bottom',90)
place('J4',-13.7,-16,'bottom');place('J5',-13.9,-8.8,'bottom',90);place('J7',-9.25,-16.05,'bottom',180)
place('J6',-14,-11,'top',90);place('J8',-14,-3,'top',90)
place('SW1',14.8,-12.5,'top',90);place('SW2',-8,12);place('SW3',-8,8.3)
place('D2',-9.5,-9.1);place('D3',-3.5,-9.1)
const extra:Record<string,number[]>={R45:[-6,8],R47:[-7.8,4.4],R48:[-7.8,3.2],R49:[-3,7.3],R50:[-4.5,7.3],R51:[10,9.6],R52:[10,-6.8],R53:[12.7,9.8],R54:[14,9.8],R55:[15.3,9.8],R56:[-7.6,-5.1],R57:[-7.6,-6.3],C38:[-7.8,5.8],C39:[-7.8,6.7],C40:[-3.6,-1.9],C41:[-.8,7.5],C42:[-5.7,7.5],C43:[1.8,.4],C44:[9.5,8.6],C45:[9.5,-6],C46:[1.3,6.8],C47:[-9,4.3],C48:[-5,10],C49:[-1,10],C50:[-8.4,6.2],C51:[-8.4,8.2],C52:[-9.1,12],L2:[-8.4,7.2],L3:[-4.3,8.3],L4:[-9.1,11],L5:[-7.8,1.8],C53:[-9.8,-3.4],C54:[-8.4,-3.4],C55:[-5.5,-2.8],C56:[-3,-2.8],C57:[-5.5,-7.5],C58:[-3,-7.5],C59:[-6.8,-3.4],C60:[-1.7,-3.4],C61:[-1.7,-5.2],C62:[-1.7,-6.5]}
for(const [n,[x,y]]of Object.entries(extra))place(n,x,y)
place('C71',-5.2,-17.8,'bottom');place('C72',7.2,-17.8,'bottom');place('R27',6.9,-16.1,'bottom')
place('TP1',-15.6,-15.4);place('TP2',-9,1);place('TP3',10.8,10.8);place('TP4',-11.6,-1.8,'bottom');place('TP5',-11.65,3.35,'bottom');place('TP6',-11.65,2.15,'bottom')

export const pins=(ref:string)=>Object.fromEntries(Object.entries(pinNets[ref]??{}).map(([p,n])=>[`pin${p}`,`net.${n}`]))
const nc=(ref:string,count:number):any=>Array.from({length:count},(_,i)=>`pin${i+1}`).filter(p=>!pins(ref)[p])
const DisplayFpc=(props:any)=><chip manufacturerPartNumber="FH35C-19S-0.3SHW(50)" pinLabels={Object.fromEntries(Array.from({length:21},(_,i)=>[`pin${i+1}`,[`P${i+1}`]]))} footprint={<footprint>
  {Array.from({length:19},(_,i)=><React.Fragment key={i}><smtpad portHints={[`pin${i+1}`]} pcbX={2.7-i*.3} pcbY={(i+1)%2?-1.4:1.475} width={.3} height={(i+1)%2?.8:.65} shape="rect"/></React.Fragment>)}
  <smtpad portHints={['pin20']} pcbX={-3.4} pcbY={-1.125} width={.4} height={.95} shape="rect"/>
  <smtpad portHints={['pin21']} pcbX={3.4} pcbY={-1.125} width={.4} height={.95} shape="rect"/>
  <courtyardoutline outline={[{x:-3.9,y:-1.9},{x:3.9,y:-1.9},{x:3.9,y:1.9},{x:-3.9,y:1.9},{x:-3.9,y:-1.9}]}/>
</footprint>} {...props}/>
export default function ReconstructedWatch({cacheFn,placementOverride,includePours=true}:{cacheFn?:AutorouterConfig['algorithmFn'],placementOverride?:Record<string,any>,includePours?:boolean}={}){
  const at=(n:string)=>(placementOverride??placement)[n]
  const chip=(Component:any,name:string,count?:number)=><Component key={name} name={name} connections={pins(name)} {...at(name)} {...count?{noConnect:nc(name,count)}:{}} {...name==='X1'?{loadCapacitance:'12pF'}:{}} pcbStyle={{silkscreenTextVisibility:'hidden'}}/>
  return <board width="34mm" height="38mm" borderRadius="3mm" thickness="0.8mm" layers={4} title="ESP32-S3 smartwatch" doubleSidedAssembly
    fabricatorPreset="jlcpcb_standard" defaultTraceWidth="0.15mm" minTraceWidth="0.127mm" minTraceToPadEdgeClearance="0.127mm" minPadEdgeToPadEdgeClearance="0.127mm" minBoardEdgeClearance="0.3mm"
    minViaHoleDiameter="0.3mm" minViaPadDiameter="0.45mm" minViaEdgeToPadEdgeClearance="0.127mm" minTraceToHoleEdgeClearance="0.25mm" minViaHoleEdgeToViaHoleEdgeClearance="0.2mm" minPlatedHoleDrillEdgeToDrillEdgeClearance="0.2mm"
    allowBlindAndBuriedVias={false} isViaInPadAllowed={false} defaultViaTenting="both_sides" schAutoLayoutEnabled={false} schTraceAutoLabelEnabled
    autorouter={{preset:'auto_local',local:true,allowViaInPad:false,traceClearance:'0.127mm'}} autorouterVersion="beta_pipeline9" autorouterEffortLevel="2x">
    {cacheFn&&<autoroutingphase name="Exact geometry cache" phaseIndex={0} algorithmFn={cacheFn}/>}
    {[...new Set(Object.values(pinNets).flatMap(Object.values))].map(n=><React.Fragment key={n}><net name={n} routingPhaseIndex={cacheFn?0:undefined} isGroundNet={n==='GND'} isPowerNet={['BAT','RAW','VSYS','V3V3','V1V8','OLED_VDD','INPUT_5V','QI_5V','DOCK_5V','V3V3','VDD_RF'].includes(n)} nominalTraceWidth={['BAT','RAW','VSYS','INPUT_5V','QI_5V','DOCK_5V','SW_NODE1','SW_NODE2'].includes(n)?'0.35mm':'0.15mm'}/></React.Fragment>)}
    {chip(ESP32_S3,'U1',57)}{chip(APS6404L_3SQR_SN,'U19',8)}{chip(W25Q128JVSIQ,'U15',8)}{chip(Q22FA12800332,'X1',4)}{chip(A_2450AT18A100E,'A1',2)}
    {chip(BMI270,'U2',14)}{chip(LIS2MDLTR,'U3',12)}{chip(VEML6030,'U4',6)}{chip(BQ24074RGTT,'U6',17)}{chip(TPS63802DLAR,'U7',10)}
    <SLO201610TR47MTT name="L1" inductance="470nH" connections={pins('L1')} {...at('L1')}/>
    {chip(MAX17048G_T10,'U8',9)}{chip(DRV2605LDGSR,'U9',10)}{chip(AP2112K_3_3TRG1,'U10',5)}{chip(TPS7A0218PDBVR,'U11',5)}{chip(TLV75801PDBVR,'U12',5)}{chip(ZSPD4003LG,'U14',13)}
    {chip(TLV7041DCKR,'U17',5)}{chip(TLV7041DCKR,'U18',5)}{chip(BSS138,'Q3',3)}{chip(BSS138,'Q4',3)}{chip(LESD5Z5_0CT1G,'D1',2)}{chip(SS14,'D2',2)}{chip(SS14,'D3',2)}
    {chip(BQ51013BRHLR,'U20',21)}{chip(DisplayFpc,'J1',21)}{chip(StandardJstUartUpward,'J5',5)}{chip(B3U_3000P,'SW1',2)}{chip(KMR221GLFS,'SW2',5)}{chip(KMR221GLFS,'SW3',5)}
    {chip(DockPads,'J2',2)}{['J3','J4','J6','J7','J8'].map(n=>chip(WirePads,n,2))}
    {resistors.map(p=><resistor key={p.name} name={p.name} resistance={p.value} manufacturerPartNumber={p.mpn} footprint={p.footprint??'0402'} supplierPartNumbers={p.part?{jlcpcb:[p.part]}:undefined} connections={pins(p.name)} {...at(p.name)}/>)}
    {capacitors.map(p=><capacitor key={p.name} name={p.name} capacitance={p.value} manufacturerPartNumber={p.mpn} footprint={p.footprint??'0402'} supplierPartNumbers={p.part?{jlcpcb:[p.part]}:undefined} connections={pins(p.name)} {...at(p.name)}/>)}
    {inductors.map(p=>p.isZeroOhm?<resistor key={p.name} name={p.name} resistance="0" manufacturerPartNumber={p.mpn} supplierPartNumbers={{jlcpcb:[p.part]}} footprint={p.footprint} connections={pins(p.name)} {...at(p.name)}/>:<inductor key={p.name} name={p.name} inductance={p.value} manufacturerPartNumber={p.mpn} footprint={p.footprint} supplierPartNumbers={p.part?{jlcpcb:[p.part]}:undefined} connections={pins(p.name)} {...at(p.name)}/>)}
    {[1,2,3,4,5,6].map(i=>chip(ProbePad,`TP${i}`,1))}
    <keepout shape="rect" pcbX={-13.45} pcbY={12.2} width={6.5} height={13.5} layers={['top','inner1','inner2','bottom']} excludeRefs={['.A1']}/>
    {/* Extra pour margin preserves the 0.127 mm rule after polygon tessellation. */}
    {includePours&&['top','inner1','inner2','bottom'].map(layer=><React.Fragment key={layer}><copperpour name={`GROUND_${layer}`} layer={layer as any} connectsTo={layer==='inner2'?'net.V3V3':'net.GND'} clearance="0.130mm" boardEdgeMargin="0.325mm"/></React.Fragment>)}
  </board>
}