Interesting result and a nice transistor.
1/f corner is a decade lower than specified by Toshiba. 12pA/rtHz at 1Hz is only 10dB above OP37/LT1037, despite 50x higher collector current. It seems that good discretes can have a meaningful advantage over IC opamps in this area.
So it's still unclear where the unexpected noise comes from? I suppose you don't know what setup TI used to produce the datasheet figures?
edit
What software are you using to produce these plots? I'm trying to do some noise measurements and already wrote an Octave/Matlab script for it, but I'm not 100% confident in my DSP skills so it would help me to compare against known-good solutions.
I just use python - scipy for the FFT and matplotlib for the plots. Pandas for the csv stuff. Here is the code, but it is not pretty. I am ashamed to admit I usually just edit the display_spectrum function as needed. I have noticed that there is some dependence on how the lower end of the spectrum looks for different values of nperseg with the signal.welch function. It may be more accurate to make it correspond to more than two periods for the lowest frequency, but this is a good compromise between resolution at the low end and jaggedness at the top end. Also, the range of csv formats it accepts is pathetic. I don't know how to make that part better and I haven't really had the motivation to learn. Nonetheless, when I have checked this against datasheet plots or the implementation in Waveforms (for the ADP3450), it is reproduces them well. The Digilent spectrum analyzer seems to give a gain error of -12% very consistently, but when I export the time domain data from the scope and analyze it in python it gives accurate values of NSD, so it's not that the front end is out of calibration. I don't know why this is, so I only really use that FFT implementation if the absolute values don't matter.
import pandas as pd
from scipy import signal
import numpy as np
import matplotlib.pyplot as plt
class Wave:
df = []
timeseries = []
gain = 1
sfreq = 0
timestep = 0
vsd_smoothed = []
freq_smoothed = []
interval = 0
vsd_check = []
freq_check = []
#voltnames and secnames are given as lists to enable matching with variously labeled
#and formatted input data
voltnames = ["v", "volt", "volts","voltage"]
secnames = ["s", "sec", "seconds", "t", "time"]
def __init__(self):
self.fft = pd.DataFrame(columns = ("psd", "vsd", "freq", "dBc"), dtype = float)
def readCSV(self, filename):
self.df = pd.read_csv(filename, skipinitialspace = True, usecols = lambda x: x.lower() in self.voltnames) #Populate dataframe from csv
self.df = self.df.assign(nV = self.df.mul(10**9)/self.gain) #convert volts to nV
self.df = self.df.rename(columns={self.df.columns[0]: "V"}, errors="raise") #update column label
self.timeseries = pd.read_csv(filename, skipinitialspace= True, usecols = lambda x: x.lower() in self.secnames, nrows=1000) #Get limited timeseries data
if self.timeseries.size > 0:
step = self.timeseries.diff()[self.timeseries.columns[0]].mean() #calculate the average difference between times
self.set_timestep(step) #set timestep
def save_trace_to_csv(self, filename):
self.df.to_csv(path_or_buf = filename, columns = ["V"])
def save_fft_to_csv(self, filename):
self.df.to_csv(path_or_buf = filename, columns = ["vsd", "freq"])
def set_timestep(self, timestep):
self.timestep = timestep
self.sfreq = 1/self.timestep
def set_gain(self, gain):
self.gain = gain
def set_sfreq(self, sfreq):
if sfreq <= 0:
print("Error: Invalid Frequency")
return 1
else:
self.sfreq = sfreq
self.timestep = 1/self.sfreq
return 0
def calc_fft(self, f_low):
if self.df.size == 0 or self.timestep == 0:
return 1
nperseg = 2*(1/(f_low*self.timestep))
self.fft["freq"], self.fft["psd"] = signal.welch(self.df["nV"], fs=self.sfreq, nperseg = nperseg, window="hamming", average="median")
self.fft["vsd"] = np.sqrt(self.fft["psd"])
def calc_rms(self, f_low, f_high):
i = int(0)
rms = float(0)
freq_step = self.fft["freq"][1] - self.fft["freq"][0]
while self.fft["freq"][i] < f_low:
i += 1
while self.fft["freq"][i] <= f_high:
i += 1
rms += self.fft["psd"][i]*freq_step
return np.sqrt(rms)
def carrier_dens(self):
return max(self.fft["vsd"])
def display_spectrum(self, title=None):
plt.figure(dpi=600)
plt.plot(self.fft["freq"], self.fft["vsd"])
plt.xscale("log")
plt.yscale("log")
#plt.ylim(top=10,bottom=0.1)
plt.xlim(left=9,right=18)
plt.xlabel('frequency [Hz]')
plt.ylabel('Linear spectrum [nV/rt Hz]')
plt.grid(visible=True, which="both", axis="both")
plt.title(title)
plt.show()
def average_readings(self, numavg):
self.timestep = self.timestep*numavg
for i in range(0, int(len(self.df["nV"])/numavg-1)):
avg = 0
for j in range(0, numavg):
avg += self.df["nV"][i*numavg + j]
self.df["nV"][i] = avg/numavg
avg = 0
def plot_tdomain(self, time_start, time_stop, title):
time = []
nv = []
lastpt = int(time_stop/self.timestep)
firstpt = int(time_start/self.timestep)
for i in range(firstpt, lastpt):
time.append((i-firstpt)*self.timestep)
nv.append(self.df["nV"][i])
i += 1
plt.figure(dpi=600)
plt.plot(time, nv)
plt.ylim(top=510,bottom=480)
#plt.xlim(left=0,right=time)
plt.xlabel('Time (s)')
plt.ylabel('nV')
plt.grid(visible=True, which="both", axis="both")
plt.title(title)
plt.show()
ppnoise = max(nv) - min(nv)
print("Peak to peak noise (nV): ", ppnoise)
w = Wave()
w.set_gain(10000)
w.readCSV("Experiments\HN4C51_6k745_Rs_50SPS_128kSa.csv")
w.set_sfreq(50)
w.calc_fft(0.01)
TI has a datasheet figure about how they measure the AC parameters, including NSD. I took a screenshot and attached it.
As for the noise sources, I have not checked the PNP duals (HN4A51). The current noise from that could be problematic. However, this is otherwise a very good transistor for current mirrors because it has as high an Early voltage as any small-signal PNP transistor I am aware of.
I took some more spectra for the HN4C51, and I was able to get a NF of about 2.5 dB with Rs = 1 (Ie had dropped to 5.68 mA at that point from battery discharge), so I played around with the model a little bit to get it to better reflect what I had seen. The one below reproduces the noise spectrum with Rs = 6k745 very well, but it is a bit too optimistic with the 1/f noise at Rs = 1R00. I added a value for KF and increased RB to 35. I added AF=1, which is the default value, though my calculation of the 1/f slope suggested 0.95. But the limited data I was working with, it seemed a bit shaky to reject the null hypothesis there. I wouldn't take this as gospel, but Toshiba hasn't gone to the trouble, so here we are. To be fair to Toshiba, the KF parameter is used in none of the BJT models included with LTSpice. The default value is zero, so none should show 1/f noise. I tested a few to check this, and all had flat NSD at low frequency.
.MODEL HN4C51 NPN(
+ LEVEL = 1
+ Kf = 0.0085f
+ Af = 1
+ IS = 1e-013
+ BF = 350
+ NF = 1
+ VAF = 276.3
+ IKF = 0.26
+ ISE = 1.37E-012
+ NE = 1.8
+ BR = 2.3
+ NR = 1
+ VAR = 100
+ IKR = 10
+ ISC = 2e-010
+ NC = 1.6
+ NK = 0.58
+ RE = 0.2
+ RB = 35
+ RC = 0.12
+ CJE = 1.435E-011
+ VJE = 0.75
+ MJE = 0.33
+ CJC = 5.714e-012
+ VJC = 0.74498
+ MJC = 0.22504
+ FC = 0.5
+ TF = 4.6e-010
+ XTF = 10
+ VTF = 5
+ ITF = 0.1
+ PTF = 0
+ TR = 10E-09
+ EG = 1.11
+ XTB = 1.4
+ XTI = 4
+ TRC1 = 0.01
+ TNOM = 25)
I think the major discrepancy between the simulation and reality is due to TI's model being too optimistic. With this bare-bones common source amplifier in LTSpice to check the noise (and compare with datasheet values), The 1/f corner is about 3 Hz, and the NSD at 100 mHz is only 6 nV/rtHz. Compare that to the datasheet noise spectral densities, where the 1/f corner is closer to 20 Hz. Simply editing the value of KF in their model from 0.2e-18 to 2e-18 actually gets you pretty close to the datasheet, but those spectra are too pessimistic. If you plug values into my LTSpice model, you can get an upper bound on KF of 0.5e-18 based on comparison to my data, and with two of these amplifiers built and tested (with indistinguishable 1/f noise), that is averaging 32 pairs of JFETs. That said, the noise figure of the JFETs in this topology is the matter at hand, so it is probably best to build something up to test it with less uncertainty. Perhaps rigging up one as a follower with a drain current of 400 uA would do the trick - the NF should be ~9 dB through the whole spectrum (a bit less because of Rg above the 1/f corner) and there's no issue of calibrating the gain.
Edit: Attached screencaps of the LTSpice simulation and the datasheet noise spectra.