Advertisements

Search Results for: linear fit

MPCNC Drag Knife: LM12UU Linear Bearing

The anodized body of the drag knife on the left measures exactly 12.0 mm OD:

Drag Knife holders - detail
Drag Knife holders – detail

Which happy fact suggested I might be able to use a standard LM12UU linear bearing, despite the obvious stupidity of running an aluminum “shaft” in a steel-ball bearing race:

Drag Knife - LM12UU holder - solid model
Drag Knife – LM12UU holder – solid model

The 12 mm section extends about halfway through the bearing, with barely 3 mm extending out the far end:

Drag Knife - LM12UU - knife blade detail
Drag Knife – LM12UU – knife blade detail

Because the knife body isn’t touching the bearing for the lower half of its length, it’ll probably deflect too much in the XY plane, but it’s simple enough to try out.

As before, the knife body’s flange is a snug fit in the hole bored in the upper disk:

Drag Knife - spring plate test fit
Drag Knife – spring plate test fit

This time, I tried faking stripper bolts by filling the threads of ordinary socket head cap screws with epoxy:

Ersatz stripper bolts - epoxy fill
Ersatz stripper bolts – epoxy fill

Turning the filled section to match the thread OD showed this just wasn’t going to work at all, so I turned the gunked section of the threads down to about 3.5 mm and continued the mission:

Drag Knife - LM12UU holder - assembled
Drag Knife – LM12UU holder – assembled

Next time, I’ll try mounting the disk on telescoping brass tubing nested around the screws. The motivation for the epoxy nonsense came from the discovery that real stainless steel stripper bolts run five bucks each, which means I’m just not stocking up on the things.

It slide surprisingly well on the cut-down screws, though:

Drag Knife - applique templates
Drag Knife – applique templates

Those appliqué templates came from patterns for a block in one of Mary’s current quilting projects, so perhaps I can be of some use whenever she next needs intricate cutouts.

The OpenSCAD source code as a GitHub Gist:

Advertisements

, , ,

6 Comments

MPCNC Drag Knife: PETG Linear Bearing

Having reasonable success using a 12 mm hole bored in a 3D printed mount for the nice drag knife holder on the left, I thought I’d try the same trick for the raw aluminum holder on the right side:

Drag Knife holders - detail
Drag Knife holders – detail

The 11.5 mm body is long enough to justify making a longer holder with more bearing surface:

Drag Knife Holder - 11.5 mm body - Slic3r preview
Drag Knife Holder – 11.5 mm body – Slic3r preview

Slicing with four perimeter threads lays down enough reasonably solid plastic to bore the central hole to a nice sliding fit:

Drag Knife - 11.5 mm body - boring
Drag Knife – 11.5 mm body – boring

The top disk gets bored to a snug press fit around the flange and upper body:

Drag Knife - 11.5 mm body - flange boring
Drag Knife – 11.5 mm body – flange boring

Assemble with springs and it pretty much works:

Drag Knife - hexagon depth setting
Drag Knife – hexagon depth setting

Unfortunately, it doesn’t work particularly well, because the two screws tightening the MPCNC’s DW660 tool holder (the black band) can apply enough force to deform the PETG mount and lock the drag knife body in the bore, while not being quite tight enough to prevent the mount from moving.

I think the holder for the black knife (on the left) worked better, because:

  • The anodized surface is much smoother & slipperier
  • The body is shorter, so less friction

In any event, I reached a sufficiently happy compromise for some heavy paper / light cardboard test shapes, but a PETG bearing won’t suffice for dependable drag knife cuttery.

Back to the laboratory …

, ,

Leave a comment

MPCNC: Linear Bearing Pen Holder

The simplest way to push a pen (or similar thing) downward with constant force may be to hold it in a linear bearing with a weight on it, so I gimmicked up a proof-of-concept. The general idea is to mount the pen so its axis coincides with the DW660 spindle, so as to have the nib trace the same path:

DW660 Pen Holder - unweighted

DW660 Pen Holder – unweighted

The puck mimics the shape of the DW660 snout closely enough to satisfy the MPCNC’s tool holder:

DW660 Pen Holder - Slic3r

DW660 Pen Holder – Slic3r

The pen holder suffers from thin walls constrained by the 10 mm (-ish) pen OD and the 12 mm linear bearing ID, to the extent the slight infill variations produced by the tapered pen outline change the OD. A flock of 16 mm bearings, en route around the planet even as I type, should provide more meat.

In any event, 3D printing isn’t noted for its perfect surface finish, so I applied an epoxy layer and rotated the holder as it cured:

DW660 Pen Holder - epoxy coating

DW660 Pen Holder – epoxy coating

After letting it cure overnight, I ran a lathe tool along the length to knock down the high spots and set the OD to 11.9+ mm. Although the result turns out to be a surprisingly nice fit in the bearing, there’s no way epoxy can sustain the surface load required for the usual precision steel-on-steel fit.

A plastic pen in a plastic holder weighs 8.3 g, which isn’t quite enough to put any force on the paper. Copper weighs 9 g/cm³ = 9 mg/mm³ and 10 AWG wire is 2.54 mm OD = 5 mm², so it’s 45 mg/mm: to get 20 g, chop off 450 mm of wire.

I chopped off a bit more than that, straightened it, annealed it, and wound it around a random contestant from the Bucket o’ Sticks with an OD just over the pen OD:

DW660 Pen Holder - copper weight forming

DW660 Pen Holder – copper weight forming

The helix is 13.5 mm down the middle of the turns and 14 turns long (trimmed of the tail going into the chuck and fudging the tail sticking out as a partial turn), so it’s 593 mm long and should weigh 26.7 g. It actually weighs 27.6 g: close enough.

Which is enough to overcome stiction due to the holder’s surface roughness, but the mediocre epoxy-on-balls fit allows the pen point to wander a bit too much for good results.

The prospect of poking precise holes into 16 mm drill rod seems daunting, but, based on what I see here, it will produce much better results: rapid prototyping FTW!

The OpenSCAD source code as a GitHub Gist:

, ,

7 Comments

AD9850 DDS Module: 125 MHz Oscillator vs. Temperature, Linear Edition

A day of jockeying the AD9850 DDS oscillator shows an interesting relation between the frequency offset and the oscillator temperature:

DDS Oscillator Frequency Offset vs. Temperature - complete

DDS Oscillator Frequency Offset vs. Temperature – complete

Now, as it turns out, the one lonely little dot off the line happened just after I lit the board up after a tweak, so the oscillator temperature hadn’t stabilized. Tossing it out produces a much nicer fit:

DDS Oscillator Frequency Offset vs. Temperature

DDS Oscillator Frequency Offset vs. Temperature

Looks like I made it up, doesn’t it?

The first-order coefficient shows the frequency varies by -36 Hz/°C. The actual oscillator frequency decreases with increasing temperature, which means the compensating offset must become more negative to make the oscillator frequency variable match reality. In previous iterations, I’ve gotten this wrong.

For example, at 42.5 °C the oscillator runs at:

125.000000 MHz - 412 Hz = 124.999588 MHz

Dividing that into 232 = 34.35985169 count/Hz, which is the coefficient converting a desired frequency into the DDS delta phase register value. Then, to get 10.000000 MHz at the DDS output, you multiply:
10×106 × 34.35985169 = 343.598517×106

Stuff that into the DDS and away it goes.

Warmed half a degree to 43.0 °C, the oscillator runs at:

125.000000 MHz - 430 Hz = 124.999570 MHz

That’s 18 Hz lower, so the coefficient becomes 34.35985667, and the corresponding delta phase for a 10 MHz output is 343.598567×106.

Obviously, you need Pretty Good Precision in your arithmetic to get those answers.

After insulating the DDS module to reduce the effect of passing breezes, I thought the oscillator temperature would track the ambient temperature fairly closely, because of the more-or-less constant power dissipation inside the foam blanket. Which turned out to be the case:

DDS Oscillator Temperature vs. Ambient

DDS Oscillator Temperature vs. Ambient

The little dingle-dangle shows startup conditions, where the oscillator warms up at a constant room temperature. The outlier dot sits 0.125 °C to the right of the lowest pair of points, being really conspicuous, which was another hint it didn’t belong with the rest of the contestants.

So, given the ambient temperature, the oscillator temperature will stabilize at 0.97 × ambient + 20.24, which is close enough to a nice, even 20 °C hotter.

The insulation blanket reduces short-term variations due to breezes, which, given the -36 Hz/°C = 0.29 ppm temperature coefficient, makes good sense; you can watch the DDS output frequency blow in the breeze. It does, however, increase the oscillator temperature enough to drop the frequency by 720 Hz, so you probably shouldn’t use the DDS oscillator without compensating for at least its zero-th order offset at whatever temperature you expect.

Of course, that’s over a teeny-tiny temperature range, where nearly anything would be linear.

The original data:

DDS Oscillator offset vs temperature - 2017-06-24

DDS Oscillator offset vs temperature – 2017-06-24

, ,

1 Comment

Victoreen 710-104 Ionization Chamber: Revised Fittings

Second time’s the charm:

Victoreen 710-104 Ionization Chamber Fittings - Show V2

Victoreen 710-104 Ionization Chamber Fittings – Show V2

There’s not much difference from the first iteration, apart from a few code cleanups. The engraved text is kinda-sorta gratuitous, but I figured having the circuit board dimensions on all the key parts would avoid heartache & confusion; the code now autosizes the board to the holder OD. Skeletonizing the board template didn’t save nearly as much printing time as I expected, though.

Now I can build a second electrometer amp without dismantling the two-transistor version.

The OpenSCAD source code:

// Victoreen 710-104 Ionization Chamber Fittings
// Ed Nisley KE4ZNU August 2015

Layout = "Show";
					// Show - assembled parts
					// Build - print can parts + shield
					// BuildShield - print just the shield
					// BuildHolder - print just the can cap & PCB base
					// CanCap - PCB insulator for 6-32 mounting studs
					// CanBase - surrounding foot for ionization chamber
					// CanRim - generic surround for either end of chamber
					// PCB - template for cutting PCB sheet
					// PCBBase - holder for PCB atop CanCap
					// Shield - electrostatic shield shell

//- Extrusion parameters must match reality!
//  Print with 2 shells and 3 solid layers

ThreadThick = 0.25;
ThreadWidth = 0.40;

HoleWindage = 0.2;

Protrusion = 0.1;			// make holes end cleanly

AlignPinOD = 1.75;			// assembly alignment pins = filament dia

inch = 25.4;

function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);

//- Screw sizes

Tap4_40 = 0.089 * inch;
Clear4_40 = 0.110 * inch;
Head4_40 = 0.211 * inch;
Head4_40Thick = 0.065 * inch;
Nut4_40Dia = 0.228 * inch;
Nut4_40Thick = 0.086 * inch;
Washer4_40OD = 0.270 * inch;
Washer4_40ID = 0.123 * inch;


//----------------------
// Dimensions

OD = 0;											// name the subscripts
LENGTH = 1;

Chamber = [91.0,38];							// Victoreen ionization chamber dimensions

Stud = [										// stud welded to ionization chamber lid
	[6.5,IntegerMultiple(0.8,ThreadThick)],		// flat head -- generous clearance
	[4.0,9.5],									// 6-32 screw -- ditto
];
NumStuds = 3;									// this really isn't much of a variable...
StudAngle = 360/NumStuds;
StudSides = 6;									// for hole around stud

BCD = 2.75 * inch;								// mounting stud bolt circle diameter

PlateThick = 2.0;								// minimum layer atop and below chamber ends
RimHeight = 4.0;								// extending along chamber perimeter
WallHeight = RimHeight + PlateThick;
WallThick = 3.0;								// thick enough to be sturdy & printable
CapSides = 8*6;									// must be multiple of 4 & 3 to make symmetries work out right

RimOD = Chamber[OD] + 2*WallThick;

echo(str("Rim OD: ",RimOD));

//PCBFlatsOD = 82.0;							// desired hex dia flat-to-flat
PCBFlatsOD = floor(RimOD*cos(30)) - 2.0;		//  .. maximum possible
//PCBFlatsOD = floor(Chamber[OD]*cos(30)) - 2.0;	//  .. chamber fitting
PCBClearance = ThreadWidth;						// clearance beyond each flat for mounting

PCBThick = 1.1;
PCBActual = [PCBFlatsOD/cos(30),PCBThick];		// OD = tip-to-tip
PCBCutter = [(PCBFlatsOD + 2*PCBClearance)/cos(30),PCBThick - ThreadThick];		// OD = tip-to-tip dia + clearance

PCBSize = str(PCBFlatsOD, " mm");
echo(str("Actual PCB across flats: ",PCBFlatsOD));
echo(str(" ... tip-to-tip dia: ",PCBActual[OD]));
echo(str(" ... thickness: ",PCBActual[LENGTH]));

HolderHeight = 13.0 + PCBCutter[LENGTH];		// thick enough for PCB to clear studs + batteries
HolderShelf = 2.0;								// shelf under PCB edge
HolderTrim = 5.0;								// remove end of holder to clear PCB edge solder blobs
echo(str("Holder trim distance: ",HolderTrim));
HolderTrimAngle = StudAngle/2 - 2*atan(HolderTrim*cos(StudAngle/2)/(PCBActual[OD]/2));	// atan is close for small angles
echo(str(" ... angle: ",HolderTrimAngle));

PinAngle = 15;									// alignment pin angle on either side of holder screw

echo(str("PCB holder across flats: ",PCBCutter[OD]*cos(30)));
echo(str(" ... height: ",HolderHeight));

ShieldInset = 0.5;								// shield inset from actual PCB flat
ShieldWall = 2.0;								// wall thickness
ShieldLid = 6*ThreadThick;						// top thickness (avoid one infill layer)
Shield = [(PCBFlatsOD - 2*ShieldInset)/ cos(30),40.0];		// electrostatic shield shell dimensions

TextSize = 4;
TextCharSpace = 1.05;
TextLineSpace = TextSize + 2;
TextDepth = 1*ThreadThick;

//----------------------
// Useful routines

module PolyCyl(Dia,Height,ForceSides=0) {			// based on nophead's polyholes

  Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);

  FixDia = Dia / cos(180/Sides);

  cylinder(r=(FixDia + HoleWindage)/2,
			h=Height,
		$fn=Sides);
}

//- Locating pin hole with glue recess
//  Default length is two pin diameters on each side of the split

module LocatingPin(Dia=AlignPinOD,Len=0.0) {
	
	PinLen = (Len != 0.0) ? Len : (4*Dia);
	
	translate([0,0,-ThreadThick])
		PolyCyl((Dia + 2*ThreadWidth),2*ThreadThick,4);

	translate([0,0,-2*ThreadThick])
		PolyCyl((Dia + 1*ThreadWidth),4*ThreadThick,4);
		
	translate([0,0,-Len/2])
		PolyCyl(Dia,Len,4);

}

module ShowPegGrid(Space = 10.0,Size = 1.0) {

  RangeX = floor(100 / Space);
  RangeY = floor(125 / Space);
  
	for (x=[-RangeX:RangeX])
		for (y=[-RangeY:RangeY])
		translate([x*Space,y*Space,Size/2])
			%cube(Size,center=true);
}

//-----

module CanRim(BaseThick) {
	
	difference() {
		cylinder(d=Chamber[OD] + 2*WallThick,h=(WallHeight + BaseThick),$fn=CapSides);
		translate([0,0,BaseThick])
			PolyCyl(Chamber[OD],Chamber[LENGTH],CapSides);
	}
	
}

module CanCap() {
	
	difference() {
		CanRim(PlateThick + Stud[0][LENGTH]);
		
		translate([0,0,-Protrusion])											// central cutout
			rotate(180/6)
				cylinder(d=BCD,h=Chamber[LENGTH],$fn=6);						//  ... reasonable size
			
		for (i=[0:(NumStuds - 1)])												// stud clearance holes
			rotate(i*StudAngle)
				translate([BCD/2,0,0])
					rotate(180/StudSides) {
						translate([0,0,PlateThick])
							PolyCyl(Stud[0][OD],Chamber[LENGTH],StudSides);
						translate([0,0,-Protrusion])
							PolyCyl(Stud[1][OD],Chamber[LENGTH],StudSides);
					}
					
		for (i=[0:(NumStuds - 1)], j=[-1,1])									// PCB holder alignment pins
			rotate(i*StudAngle + j*PinAngle + 60)
				translate([Chamber[OD]/2,0,0])
					rotate(180/4 - j*PinAngle)
						LocatingPin(Len=2*(PlateThick + Stud[0][LENGTH]) - 4*ThreadThick);
						
		translate([-(BCD/2),0,-Protrusion])
			rotate(90) mirror() 
				linear_extrude(height=(ThreadThick + Protrusion))
				text(PCBSize,size=6,font="Liberation Mono:style=bold",halign="center",valign="center");
	}

}

module CanBase() {
	
	difference() {
		CanRim(PlateThick);
		translate([0,0,-Protrusion])
			PolyCyl(Chamber[OD] - 2*RimHeight,Chamber[LENGTH],CapSides);
	}
}

module PCBTemplate() {
	
	CutLen = 10*PCBActual[LENGTH];
	
	difference() {
		cylinder(d=PCBActual[OD],h=PCBActual[LENGTH],$fn=6);		// actual PCB size
		translate([0,0,-Protrusion])
			cylinder(d=8,h=CutLen,$fn=12);
		if (true)
			for (i=[0:5])											// empirical cutouts
				rotate(i*60 + 30)
					translate([PCBFlatsOD/3,0,-Protrusion])
						rotate(60)
							cylinder(d=0.43*PCBActual[OD],h=CutLen,$fn=3);
							
		translate([PCBActual[OD]/4,0,(PCBActual[LENGTH] - ThreadThick)])
			linear_extrude(height=(ThreadThick + Protrusion),convexity=1)
			text(PCBSize,size=4,font="Liberation Mono:style=bold",halign="center",valign="center");
							
	}
}

module PCBBase() {

	intersection() {
		difference() {
			cylinder(d=Chamber[OD] + 2*WallThick,h=HolderHeight,$fn=CapSides);		// outer rim
			
			rotate(30) {
				translate([0,0,-Protrusion])										// central hex
					cylinder(d=(PCBActual[OD] - HolderShelf/cos(30) - HolderShelf/cos(30)),h=2*HolderHeight,$fn=6);
					
				translate([0,0,HolderHeight - PCBCutter[LENGTH]])					// hex PCB recess
					cylinder(d=PCBCutter[OD],h=HolderHeight,$fn=6);
					
				for (i=[0:NumStuds - 1])											// PCB retaining screws
					rotate(i*StudAngle + 180/(2*NumStuds))
						translate([(PCBCutter[OD]*cos(30)/2 + Clear4_40/2 + ThreadWidth),0,-Protrusion])
							rotate(180/6)
								PolyCyl(Tap4_40,2*HolderHeight,6);
								
				for (i=[0:(NumStuds - 1)], j=[-1,1])								// PCB holder alignment pins
					rotate(i*StudAngle + j*PinAngle + 180/(2*NumStuds))
						translate([Chamber[OD]/2,0,0])
							rotate(180/4 - j*PinAngle)
								LocatingPin(Len=2*(HolderHeight - 4*ThreadThick));
			}
			
			if (false)
			for (i=[0:NumStuds - 1])
				rotate(i*StudAngle - StudAngle/2)							// segment isolation - hex sides
					translate([0,0,-Protrusion]) {
						linear_extrude(height=2*HolderHeight)
							polygon([[0,0],[Chamber[OD],0],[Chamber[OD]*cos(180/NumStuds),Chamber[OD]*sin(180/NumStuds)]]);
					}
					
			translate([-(PCBFlatsOD/2 + PCBClearance - HolderShelf),0,HolderHeight/2])
				rotate([0,90,0]) rotate(90)
					linear_extrude(height=(ThreadWidth + Protrusion))
					text(PCBSize,size=6,font="Liberation Mono:style=bold",halign="center",valign="center");
					
		}
		
		for (i=[0:NumStuds - 1])
			rotate(i*StudAngle + StudAngle/2 - HolderTrimAngle/2)								// trim holder ends
				translate([0,0,-Protrusion]) {
					linear_extrude(height=2*HolderHeight)
						polygon([[0,0],[Chamber[OD],0],[Chamber[OD]*cos(HolderTrimAngle),Chamber[OD]*sin(HolderTrimAngle)]]);
				}
			
	}
}

//-- Electrostatic shield
//		the cutouts are completely ad-hoc

module ShieldShell() {
	
CutHeight = 7.0;
	
	difference() {
		cylinder(d=Shield[OD],h=Shield[LENGTH],$fn=6);							// exterior shape
		
		translate([0,0,-ShieldLid])												// interior
			cylinder(d=(Shield[OD] - 2*ShieldWall/cos(30)),h=Shield[LENGTH],$fn=6);

		translate([0,0,Shield[LENGTH] - TextDepth])
		rotate(180) {
			translate([0,0.3*Shield[OD] - 0*TextLineSpace,0])
				linear_extrude(height=(TextDepth + Protrusion))
					text("Gamma",size=TextSize,spacing=TextCharSpace,font="Liberation:style=bold",halign="center",valign="center");
			translate([0,0.3*Shield[OD] - 1*TextLineSpace,0])
				linear_extrude(height=(TextDepth + Protrusion))
					text("Ionization",size=TextSize,spacing=TextCharSpace,font="Liberation:style=bold",halign="center",valign="center");
			translate([0,0.3*Shield[OD] - 2*TextLineSpace,0])
				linear_extrude(height=(TextDepth + Protrusion))
					text("Amplifier",size=TextSize,spacing=TextCharSpace,font="Liberation:style=bold",halign="center",valign="center");
			translate([0,-0.3*Shield[OD] + 1*TextLineSpace,0])
				linear_extrude(height=(TextDepth + Protrusion))
					text("KE4ZNU",size=TextSize,spacing=TextCharSpace,font="Liberation:style=bold",halign="center",valign="center");
			translate([0,-0.3*Shield[OD] + 0*TextLineSpace,0])
				linear_extrude(height=(TextDepth + Protrusion))
					text("2015-08",size=TextSize,spacing=TextCharSpace,font="Liberation:style=bold",halign="center",valign="center");
		}
			
		translate([Shield[OD]/4 - 20/2,Shield[OD]/2,(CutHeight - Protrusion)/2])	// switch
			rotate(90)
				cube([Shield[OD],20,CutHeight + Protrusion],center=true);

		if (false)
		translate([-Shield[OD]/4 + 5/2,Shield[OD]/2,(CutHeight - Protrusion)/2])	// front
			rotate(90)
				cube([Shield[OD],5,CutHeight + Protrusion],center=true);

		translate([-Shield[OD]/2,0,(CutHeight - Protrusion)/2])						// right side
				cube([Shield[OD],7,CutHeight + Protrusion],center=true);

		translate([0,(Shield[OD]*cos(30)/2 - ThreadWidth),0.75*Shield[LENGTH]])
			rotate([90,0,180]) rotate(00)
				linear_extrude(height=(ThreadWidth + Protrusion))
				text(PCBSize,size=5,font="Liberation Mono:style=bold",halign="center",valign="center");
	}
	
}

//----------------------
// Build it

ShowPegGrid();

if (Layout == "CanRim") {
	CanRim();
}

if (Layout == "CanCap") {
	CanCap();
}

if (Layout == "CanBase") {
	CanBase();
}

if (Layout == "PCBBase") {
	PCBBase();
}

if (Layout == "PCB") {
	PCBTemplate();
}

if (Layout == "Shield") {
	ShieldShell();
}

if (Layout == "Show") {
	CanBase();
	color("Orange",0.5)
		translate([0,0,PlateThick + Protrusion])
			cylinder(d=Chamber[OD],h=Chamber[LENGTH],$fn=CapSides);
	translate([0,0,(2*PlateThick + Chamber[LENGTH] + 2*Protrusion)])
		rotate([180,0,0])
			CanCap();
	translate([0,0,(2*PlateThick + Chamber[LENGTH] + 5.0)])
		PCBBase();
	color("Green",0.5)
		translate([0,0,(2*PlateThick + Chamber[LENGTH] + 7.0 + HolderHeight)])
			rotate(30)
				PCBTemplate();
	translate([0,0,(2*PlateThick + Chamber[LENGTH] + 15.0 + HolderHeight)])
		rotate(-30)
			ShieldShell();}

if (Layout == "Build") {
	
	translate([-0.50*Chamber[OD],-0.60*Chamber[OD],0])
		CanCap();
		
	if (false)
		translate([0.55*Chamber[OD],-0.60*Chamber[OD],0])
			rotate(30)
				translate([0,0,Shield[LENGTH]])
					rotate([0,180,0])
						ShieldShell();
	if (true)
		translate([0.55*Chamber[OD],-0.60*Chamber[OD],0])
			rotate(30)
				PCBTemplate();

	if (true)
		translate([-0.25*Chamber[OD],0.60*Chamber[OD],0])
			CanBase();
		translate([0.25*Chamber[OD],0.60*Chamber[OD],0])
			PCBBase();
}

if (Layout == "BuildHolder") {
	translate([-0.25*Chamber[OD],0,0])
		CanCap();
	translate([0.25*Chamber[OD],0,0])
		PCBBase();
}

if (Layout == "BuildShield") {
	
	translate([0,0,Shield[LENGTH]])
		rotate([0,180,0])
				ShieldShell();
		

}

,

2 Comments

Thermistor Linearization

Faced with the need to measure heatsink temperature in an Arduino project and being unwilling to putz around with a MAX6675 thermocouple amp, I found a bag of thermistors in the heap. Unlike most surplus, the bag pedigreed them as Semitec 103CT-4, which led to some relevant parameters:

  • T0 = 25 °C
  • R0 = 10 kΩ
  • B = 3270 K

The equation for a thermistor’s resistance at a given temperature (in K, not °C) is:

R = R0 * e(B/T - B/T0)

The canonical Arduino thermistor circuit uses a series resistor with a value equal to R0:

Thermistor Linearization - Rseries

Thermistor Linearization - Rseries

Setting Rseries = 10 KΩ and applying a bit of spreadsheet-fu produces this:

Thermistor Linearization - Rseries - Graph

Thermistor Linearization - Rseries - Graph

Getting within +2 °C /-1 °C over -20 °C to 60 °C isn’t all that bad, but … I wondered whether there might be an easy way to get better linearization. The heatsink temperature will range from about -10 °C to 60 °C (yes, there will be a Peltier cooler involved), so the range is a bit broader than usual.

A bit of diligent rummaging turned up that description, which led to US Patent 3,316,765 from back in 1967, which teaches the concept of two different thermistors, one for low temperatures and one for high temperatures, with some resistive blending:

Patent 3316765 Fig 3

Patent 3316765 Fig 3

The patent includes the claim of many different thermistors, each with a series resistor, to cover a much broader temperature range.

Given a bag of identical thermistors, I wondered what might be possible. A bit more spreadsheet-fu produced this:

Thermistor Linearization - Dual Thermistors - Graph

Thermistor Linearization - Dual Thermistors - Graph

Which corresponds to this sketch, with Rseries = 6.2 kΩ, R1 = 27 kΩ, and R2 = 0.0:

Thermistor Linearization - Dual Thermistors

Thermistor Linearization - Dual Thermistors

All in all, a nicely centered ±1 °C error from -15 °C to +60 °C can’t be beat. The output voltage even spans 0.13 to 0.71 of Vcc, about 9 of the available 10 ADC bits.

Those two resistors came from hand-tweaking with standard values, so it’s not like there’s a genetic algorithm involved. The value of Rseries wants to be a bit below the parallel combination of the two branches near 30 °C and R1 seems happiest around the 0 °C thermistor resistance. I vaguely thought about using a multivariable solver, but what’s the point?

The result seems good enough that I didn’t try three thermistors. T2, the one with R2=0, already handles the high temperature range and the low end is fine, so it seems there’s not much to be gained. If you had a stash of different thermistors and knew their characteristics, then the results would be different.

Admittedly, one could program the actual logarithmic equation to unbend a single thermistor’s voltage into temperature, but I must kludge up a thermistor mount anyway, so why not entomb two thermistors and an SMD resistor, then use a linear fit? It’s not like fancy math will give the whole lashup any greater accuracy.

The spreadsheet may be of interest. It started out as an OpenOffice spreadsheet, but WordPress doesn’t permit *.ods files, soooo it’s in MS Excel format.

4 Comments

Thermocouple Calibration: Linear Regression

With the thermistors nestled all snug in their wells, I turned on the heat and recorded the temperatures. I picked currents roughly corresponding to the wattages shown, only realizing after the fact that I’d been doing the calculation for the 5 Ω Thing-O-Matic resistors, not the 6 Ω resistor I was actually using. Doesn’t matter, as the numbers depend only on the temperatures, not the wattage.

This would be significantly easier if I had a thermocouple with a known-good calibration, but I don’t. Assuming that the real temperature lies somewhere near the average of the six measurements is the best I can do, so … onward!

Plotting the data against the average at each measurement produces a cheerful upward-and-to-the-right graph:

Data vs Ensemble Average

Data vs Ensemble Average

So the thermocouples seem reasonably consistent.

Plotting the difference between each measurement and the average of all the measurements at that data point produces this disconcertingly jaggy result:

Difference from Ensemble Average

Difference from Ensemble Average

The TOM thermocouple seems, um, different, which is odd, because the MAX6675 converts directly from thermocouple voltage to digital output with no intervening software. It’s not clear what’s going on; I don’t know if the bead was slightly out of its well or if that’s an actual calibration difference. I’ll check it later, but for now I will simply run with the measurements.

Eliminating the TOM data from the average produces a better clustering of the remaining five readings, with the TOM being even further off. The regression lines show the least-squares fit to each set of points, which look pretty good:

Difference from Average without TOM

Difference from Average without TOM

Those regression lines give the offset and slope of the best-fit line that goes from the average reading to the actual reading, but I really need an equation from the actual reading for each thermocouple to the combined average. Rather than producing half a dozen graphs, I applied the spreadsheet’s SLOPE() and INTERCEPT() functions with the average temperature as Y and the measured temperature as X.

That produced this table:

                    TOM     MPJA  Craftsman A  Craftsman B   Fluke T1  Fluke T2
M = slope        1.0534   0.5434       0.5551       0.5539     1.0112    1.0154
B = intercept   -1.6073 -15.3703     -19.4186     -16.9981    -0.7421   -0.3906

And then, given a reading from any of the thermocouples, converting that value to the average requires plugging the appropriate values from that table into good old

  • y = mx + b

For example, converting the Fluke 52 T1 readings produces this table of values. The Adjusted column shows the result of that equation and the Delta Avg column gives the difference from the average temperature (not shown here) for that reading.

Fluke T1    Adjusted   Delta Avg   Max Abs Err
21.0        20.5        -0.4          0.78
29.0        28.6        -0.3
34.8        34.4        -0.3
45.5        45.3        -0.2
50.1        49.9         0.0
52.0        51.8         0.2
69.3        69.3         0.3
76.4        76.5         0.4
78.9        79.0         0.6
107.9       108.4         0.2
112.3       112.8         0.4
117.5       118.1         0.3
127.8       128.5        -0.2
133.2       134.0         0.1
136.6       137.4         0.1
138.1       138.9         0.1
146.4       147.3        -0.4
155.8       156.8        -0.8

The Max Avg Error (the largest value of the absolute difference from the average temperature at each point) after correction is 0.78 °C for this set. The others are less than that, with the exception of the TOM thermocouple, which differs by 1.81 °C.

So now I can make a whole bunch of temperature readings, adjust them to the same “standard”, and be off by (generally) less than 1 °C. That’s much better than the 10 °C of the unadjusted readings and seems entirely close enough for what I need…

11 Comments