Implemented the photodiode device in the parameterized device
generator for magic.
diff --git a/sky130/magic/sky130.tcl b/sky130/magic/sky130.tcl
index bcf4df5..59bbc67 100644
--- a/sky130/magic/sky130.tcl
+++ b/sky130/magic/sky130.tcl
@@ -101,6 +101,8 @@
 	    "magic::gencell sky130::sky130_fd_pr__diode_pw2nd_05v5" pdk1
    magic::add_toolkit_command $layoutframe "p-diode" \
 	    "magic::gencell sky130::sky130_fd_pr__diode_pd2nw_05v5" pdk1
+   magic::add_toolkit_command $layoutframe "photodiode" \
+	    "magic::gencell sky130::sky130_fd_pr__photodiode" pdk1
 
    magic::add_toolkit_separator	$layoutframe pdk1
    magic::add_toolkit_command $layoutframe "MOS varactor" \
@@ -868,6 +870,10 @@
 	full_metal 1 vias 1 viagb 0 viagt 0 viagl 0 viagr 0}
 }
 
+proc sky130::sky130_fd_pr__photodiode_defaults {} {
+    return {nx 1 ny 1 deltax 0 deltay 0 xstep 8.0 ystep 8.0}
+}
+
 proc sky130::sky130_fd_pr__diode_pd2nw_05v5_defaults {} {
     return {w 0.45 l 0.45 area 0.2025 peri 1.8 \
 	nx 1 ny 1 dummy 0 lmin 0.45 wmin 0.45 \
@@ -927,6 +933,10 @@
     return [sky130::diode_convert $parameters]
 }
 
+proc sky130::sky130_fd_pr__photodiode_convert {parameters} {
+    return [sky130::fixed_convert $parameters]
+}
+
 proc sky130::sky130_fd_pr__diode_pd2nw_05v5_convert {parameters} {
     return [sky130::diode_convert $parameters]
 }
@@ -961,6 +971,10 @@
     sky130::diode_dialog sky130_fd_pr__diode_pw2nd_11v0 $parameters
 }
 
+proc sky130::sky130_fd_pr__photodiode_dialog {parameters} {
+    sky130::fixed_dialog $parameters
+}
+
 proc sky130::sky130_fd_pr__diode_pd2nw_05v5_dialog {parameters} {
     sky130::diode_dialog sky130_fd_pr__diode_pd2nw_05v5 $parameters
 }
@@ -995,6 +1009,10 @@
     sky130::diode_check $parameters
 }
 
+proc sky130::sky130_fd_pr__photodiode_check {parameters} {
+    sky130::fixed_check $parameters
+}
+
 proc sky130::sky130_fd_pr__diode_pd2nw_05v5_check {parameters} {
     sky130::diode_check $parameters
 }
@@ -1209,6 +1227,153 @@
 }
 
 #----------------------------------------------------------------
+# Photodiode: Draw a single device
+#----------------------------------------------------------------
+
+proc sky130::photodiode_device {parameters} {
+
+    # Set a local variable for each parameter (e.g., $l, $w, etc.)
+    foreach key [dict keys $parameters] {
+        set $key [dict get $parameters $key]
+    }
+
+    # Draw the device
+    pushbox
+    box size 0 0
+
+    # Device has ntap fixed width of 0.41 x 0.41
+    # Surrounded by nwell 0.84 x 0.84
+    # Surrounded by deep nwell 3.0 x 3.0
+
+    pushbox
+    box grow c 0.205um
+    paint nsd
+    popbox
+    pushbox
+    box grow c 0.42um
+    paint nwell
+    popbox
+    pushbox
+    box grow c 1.5um
+    paint photo
+
+    set cext [sky130::getbox]
+
+    popbox
+
+    # Only enough space for one contact
+    set w ${contact_size}
+    set l ${contact_size}
+
+    set cext [sky130::unionbox $cext [sky130::draw_contact ${w} ${l} \
+		0 ${metal_surround} ${contact_size} \
+		nsd nsc li horz]]
+
+    popbox
+    return $cext
+}
+
+#----------------------------------------------------------------
+
+proc sky130::photodiode_draw {parameters} {
+
+    # Set a local variable for each parameter (e.g., $l, $w, etc.)
+    foreach key [dict keys $parameters] {
+        set $key [dict get $parameters $key]
+    }
+
+    pushbox
+    box values 0 0 0 0
+
+    # Determine the base device dimensions by drawing one device
+    # while all layers are locked (nothing drawn).  This allows the
+    # base drawing routine to do complicated geometry without having
+    # to duplicate it here with calculations.
+
+    tech lock *
+    set bbox [sky130::photodiode_device $parameters]
+    # puts stdout "Diagnostic: Device bounding box e $bbox (um)"
+    tech unlock *
+
+    set fw [- [lindex $bbox 2] [lindex $bbox 0]]
+    set fh [- [lindex $bbox 3] [lindex $bbox 1]]
+    set lw [+ [lindex $bbox 2] [lindex $bbox 0]]
+    set lh [+ [lindex $bbox 3] [lindex $bbox 1]]
+
+    # Determine tile width and height
+
+    set dx [+ $fw $end_spacing]
+    set dy [+ $fh $end_spacing]
+
+    # Determine core width and height
+    set corex [+ [* [- $nx 1] $dx] $fw]
+    set corey [+ [* [- $ny 1] $dy] $fh]
+    set corellx [/ [+ [- $corex $fw] $lw] 2.0]
+    set corelly [/ [+ [- $corey $fh] $lh] 2.0]
+
+    # Calculate guard ring size (measured to contact center)
+    # Spacing between photodiode (deep nwell) and deep nwell (other) is 5.3um
+    set gx [+ $corex 15.965]
+    set gy [+ $corey 15.965]
+
+    pushbox
+
+    # The deep nwell is offset 0.315 from the nwell ring center to get the
+    # right overlap.  The deep nwell ring has a minimum width of 3um.
+    set hgx [/ $gx 2.0]
+    set hgy [/ $gy 2.0]
+    set dwx [+ $hgx 0.315]
+    set dwy [+ $hgy 0.315]
+    box grow e ${dwx}um
+    box grow w ${dwx}um
+    box grow n ${dwy}um
+    box grow s ${dwy}um
+    paint dnwell
+    box grow e -3.0um
+    box grow w -3.0um
+    box grow n -3.0um
+    box grow s -3.0um
+    erase dnwell
+
+    popbox
+
+    # Draw the guard ring first.  0.63 is the amount nwell surrounds contact;
+    # 0.63 * 2 + 0.17 = total nwell width 1.43um, needed to cover dnwell edge.
+    set newdict [dict create	 \
+	sub_type    space	 \
+	guard_sub_type	 nwell	 \
+	guard_sub_surround  0.63 \
+	plus_diff_type   nsd	 \
+	plus_contact_type nsc	 \
+    ]
+    set guarddict [dict merge $parameters $newdict]
+    sky130::guard_ring $gx $gy $guarddict
+
+    # Draw outside P-ring and generated the 2nd ring
+    set gx [+ $gx [* 2.0 [+ 0.56 $diff_spacing $diff_surround]] $contact_size]
+    set gy [+ $gy [* 2.0 [+ 0.56 $diff_spacing $diff_surround]] $contact_size]
+    sky130::guard_ring $gx $gy $parameters
+
+    pushbox
+    box move w ${corellx}um
+    box move s ${corelly}um
+
+    for {set xp 0} {$xp < $nx} {incr xp} {
+	pushbox
+	for {set yp 0} {$yp < $ny} {incr yp} {
+	    sky130::photodiode_device $parameters
+            box move n ${dy}um
+        }
+	popbox
+        box move e ${dx}um
+    }
+    popbox
+    popbox
+
+    tech revert
+}
+
+#----------------------------------------------------------------
 
 proc sky130::sky130_fd_pr__diode_pw2nd_05v5_draw {parameters} {
 
@@ -1422,6 +1587,31 @@
 }
 
 #----------------------------------------------------------------
+# The photodiode has its own drawing routine, so
+#----------------------------------------------------------------
+
+proc sky130::sky130_fd_pr__photodiode_draw {parameters} {
+    # Set a local variable for each rule in ruleset
+    foreach key [dict keys $sky130::ruleset] {
+        set $key [dict get $sky130::ruleset $key]
+    }
+
+    set newdict [dict create \
+	    guard		1	\
+	    sub_type		space	\
+	    end_spacing		5.0	\
+	    end_surround	1.0	\
+	    sub_spacing		5.3	\
+	    guard_sub_type	pwell	\
+	    guard_sub_surround  0.18	\
+	    plus_diff_type	psd	\
+	    plus_contact_type	psc	\
+    ]
+    set drawdict [dict merge $sky130::ruleset $newdict $parameters]
+    return [sky130::photodiode_draw $drawdict]
+}
+
+#----------------------------------------------------------------
 # Drawn capacitor routines
 # NOTE:  Work in progress.  These values need to be corrected.
 #----------------------------------------------------------------
diff --git a/sky130/magic/sky130.tech b/sky130/magic/sky130.tech
index b5546bf..7c5e949 100644
--- a/sky130/magic/sky130.tech
+++ b/sky130/magic/sky130.tech
@@ -82,6 +82,7 @@
 # sky130_fd_pr__res_iso_pw	rpw		pwell resistor (in deep nwell)
 # sky130_fd_pr__esd_nfet_g5v0d10v5 mvnfetesd	ESD thickox nFET
 # sky130_fd_pr__esd_pfet_g5v0d10v5 mvpfetesd	ESD thickox pFET
+# sky130_fd_pr__photodiode	photo		Photodiode
 #
 # (*) Note that ppres may extract into some generic type called
 # "sky130_fd_pr__res_xhigh_po", but only specific sizes of xhrpoly are
@@ -142,6 +143,7 @@
 # Deep nwell
   dwell dnwell,dnw
   dwell isosubstrate,isosub
+  dwell photodiode,photo
 
 # Wells
   well nwell,nw
@@ -474,6 +476,7 @@
   nwell     nwell
   pwell	    pwell
   rpwell    pwell 	ptransistor_stripes
+  photo	    nwell	nwell_field_implant
   ndiff     ndiffusion
   fomfill   ndiffusion
   pdiff     pdiffusion
@@ -693,7 +696,7 @@
 #-----------------------------------------------------
 
 connect
-  *nwell,*nsd,*mvnsd,dnwell,pnp *nwell,*nsd,*mvnsd,dnwell,pnp
+  *nwell,*nsd,*mvnsd,dnwell,pnp,photo *nwell,*nsd,*mvnsd,dnwell,pnp,photo
   pwell,*psd,*mvpsd,npn  pwell,*psd,*mvpsd,npn
   *li,coreli,lifill	*li,coreli,lifill
   *m1,m1fill,obsmcon	*m1,m1fill,obsmcon
@@ -751,7 +754,7 @@
 # DNWELL
 #----------------------------------------------------------------
 
- layer DNWELL	dnwell,npn
+ layer DNWELL	dnwell,npn,photo
 	calma	64 18
 
  layer PWRES    rpw
@@ -974,6 +977,9 @@
 	mask-hints PNPID
 	calma	82 44
 
+ layer PHOTO photo
+	calma	81 81
+
 #----------------------------------------------------------------
 # RPM
 #----------------------------------------------------------------
@@ -2387,6 +2393,9 @@
  and-not NWELL,nwelcheck
  and NPNID
 
+ layer photo DNWELL
+ and PHOTO
+
  layer rpw PWRES
  and DNWELL
  labels PWRES
@@ -3927,6 +3936,8 @@
  calma CAPID 82 64
  # Core area ID mark
  calma COREID 81 2
+ # Photodiode ID mark
+ calma PHOTO 81 81
  # Standard cell ID mark
  calma STDCELL 81 4
  # Padframe cell ID mark
@@ -4104,6 +4115,11 @@
 	"P+ diff cannot straddle Deep N-well (dnwell.5)"
  variants (fast),(full)
 
+ width photo 3000 "Photodiode width < %d (photo.2)"
+ spacing photo photo 5000 touching_ok "Photodiode spacing < %d (photo.3)"
+ spacing photo dnwell 5300 touching_illegal \
+	"Photodiode spacing to deep nwell < %d (photo.4)"
+
 #-----------------------------
 # NWELL
 #-----------------------------
@@ -5394,6 +5410,8 @@
  device resistor sky130_fd_pr__res_generic_m4 rm4 *m4
  device resistor sky130_fd_pr__res_generic_m5 rm5 *m5
 #endif (METAL5)
+ device ndiode   sky130_fd_pr__model__parasitic__diode_ps2dn \
+	photo pwell,space/w error a=area
 
  device rsubcircuit sky130_fd_pr__res_high_po_0p35 xhrpoly \
 	xpc pwell,space/w error +res0p35 l=l