MSP430 reflash working

This commit is contained in:
jaseg 2020-04-29 18:02:57 +02:00
parent 3dd5789800
commit 76c12726e8
16 changed files with 144 additions and 445 deletions

View file

@ -187,12 +187,23 @@ static struct jtag_img_descriptor {
} jtag_img = {
.devmem_img_start = 0x00c000,
.spiflash_img_start = 0x000000,
.img_len = 0x060000,
.img_len = 0x004000,
};
const char fw_dump[0x4000] = {
#include "EasyMeter_Q3DA1002_V3.03_fw_dump_0xc000.h"
};
ssize_t jt_spi_flash_read_block(void *usr, int addr, size_t len, uint8_t *out) {
/*
struct jtag_img_descriptor *desc = (struct jtag_img_descriptor *)usr;
return spif_read(&spif, desc->spiflash_img_start + addr, len, (char *)out);
*/
for (size_t i=0; i<len; i++)
out[i] = fw_dump[addr - 0xc000 + i];
return len;
}
static unsigned int measurement_errors = 0;
@ -240,15 +251,16 @@ int main(void)
int i=0;
while (23) {
mspd_jtag_init();
con_printf("%d flash result: %d\r\n", i,
mspd_jtag_flash_and_reset(jtag_img.devmem_img_start, jtag_img.img_len, jt_spi_flash_read_block, &jtag_img));
for (int i=0; i<168*1000*5; i++)
int rv = mspd_jtag_flash_and_reset(jtag_img.devmem_img_start, jtag_img.img_len, jt_spi_flash_read_block, &jtag_img);
con_printf("%d flash result: %d\r\n", i, rv);
while (!rv) {}
for (int j=0; j<168*1000*5; j++)
asm volatile ("nop");
i++;
}
while (23) {
if (adc_fft_buf_ready_idx != -1) {
for (int i=0; i<168*1000*2; i++)
for (int j=0; j<168*1000*2; j++)
asm volatile ("nop");
GPIOA->BSRR = 1<<11;
memcpy(adc_fft_buf[!adc_fft_buf_ready_idx], adc_fft_buf[adc_fft_buf_ready_idx] + FMEAS_FFT_LEN/2, sizeof(adc_fft_buf[0][0]) * FMEAS_FFT_LEN/2);
@ -257,8 +269,8 @@ int main(void)
bool clip_low=false, clip_high=false;
const int clip_thresh = 100;
for (size_t i=FMEAS_FFT_LEN/2; i<FMEAS_FFT_LEN; i++) {
int val = adc_fft_buf[adc_fft_buf_ready_idx][i];
for (size_t j=FMEAS_FFT_LEN/2; j<FMEAS_FFT_LEN; j++) {
int val = adc_fft_buf[adc_fft_buf_ready_idx][j];
if (val < clip_thresh)
clip_low = true;
if (val > FMEAS_ADC_MAX-clip_thresh)
@ -266,7 +278,7 @@ int main(void)
}
GPIOB->ODR = (GPIOB->ODR & ~(3<<11)) | (!clip_low<<11) | (!clip_high<<12);
for (int i=0; i<168*1000*2; i++)
for (int j=0; j<168*1000*2; j++)
asm volatile ("nop");
GPIOA->BSRR = 1<<11;

View file

@ -1,6 +1,7 @@
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include "output.h"
#include "jtaglib.h"
@ -53,6 +54,18 @@ void mspd_jtag_init() {
gpio_pin_setup(gpios[i].gpio, gpios[i].pin, gpios[i].mode, 3, 0, 0);
}
static void sr_gpio_write(int num, int out) {
if (out)
gpios[num].gpio->BSRR = 1<<gpios[num].pin;
else
gpios[num].gpio->BSRR = 1<<gpios[num].pin<<16;
}
static void sr_jtdev_rst(struct jtdev *p, int out) {
UNUSED(p);
sr_gpio_write(SR_GPIO_RST, out);
}
int mspd_jtag_flash_and_reset(size_t img_start, size_t img_len, ssize_t (*read_block)(void *usr, int addr, size_t len, uint8_t *out), void *usr)
{
union {
@ -73,18 +86,17 @@ int mspd_jtag_flash_and_reset(size_t img_start, size_t img_len, ssize_t (*read_b
if (jtag_id != 0x89 && jtag_id != 0x91)
return -EINVAL;
/* FIXME DEBUG */
#if 0
con_printf("Memory dump:\r\n");
for (size_t i=0; i<256;) {
for (size_t i=0x1000; i<=0x10ff;) {
con_printf("%04x: ", i);
for (size_t j=0; j<16; i+=2, j+=2) {
con_printf("%04x ", jtag_read_mem(&sr_jtdev, 16, 0xc000 + i));
for (size_t j=0; j<16; i+=1, j+=1) {
con_printf("%02x ", jtag_read_mem(&sr_jtdev, 8, i));
}
con_printf("\r\n");
}
return 0;
/* END DEBUG */
#endif
/* Clear flash */
jtag_erase_flash(&sr_jtdev, JTAG_ERASE_MAIN, 0);
@ -93,6 +105,7 @@ int mspd_jtag_flash_and_reset(size_t img_start, size_t img_len, ssize_t (*read_b
/* Write flash */
for (size_t p = img_start; p < img_start + img_len; p += BLOCK_SIZE) {
con_printf("Writing block %04zx\r\n", p);
ssize_t nin = read_block(usr, p, BLOCK_SIZE, block.bytes);
if (nin < 0)
@ -122,6 +135,8 @@ int mspd_jtag_flash_and_reset(size_t img_start, size_t img_len, ssize_t (*read_b
if (sr_jtdev.failed)
return -EPIPE;
jtag_release_device(&sr_jtdev, 0xfffe);
return 0;
}
@ -153,13 +168,6 @@ static void sr_jtdev_connect(struct jtdev *p) {
/* ignore */
}
static void sr_gpio_write(int num, int out) {
if (out)
gpios[num].gpio->BSRR = 1<<gpios[num].pin;
else
gpios[num].gpio->BSRR = 1<<gpios[num].pin<<16;
}
static void sr_jtdev_tck(struct jtdev *p, int out) {
UNUSED(p);
sr_gpio_write(SR_GPIO_TCK, out);
@ -175,11 +183,6 @@ static void sr_jtdev_tdi(struct jtdev *p, int out) {
sr_gpio_write(SR_GPIO_TDI, out);
}
static void sr_jtdev_rst(struct jtdev *p, int out) {
UNUSED(p);
sr_gpio_write(SR_GPIO_RST, out);
}
static void sr_jtdev_tst(struct jtdev *p, int out) {
UNUSED(p);
sr_gpio_write(SR_GPIO_TST, out);

View file

@ -1,372 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"import struct\n",
"\n",
"import numpy as np\n",
"from scipy import signal, optimize\n",
"from matplotlib import pyplot as plt\n",
"\n",
"import rocof_test_data"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"%matplotlib widget"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"fs = 1000 # Hz\n",
"ff = 50 # Hz\n",
"duration = 60 # seconds\n",
"# test_data = rocof_test_data.sample_waveform(rocof_test_data.test_close_interharmonics_and_flicker(),\n",
"# duration=20,\n",
"# sampling_rate=fs,\n",
"# frequency=ff)[0]\n",
"# test_data = rocof_test_data.sample_waveform(rocof_test_data.gen_noise(fmin=10, amplitude=1),\n",
"# duration=20,\n",
"# sampling_rate=fs,\n",
"# frequency=ff)[0]\n",
"\n",
"\n",
"#gen = rocof_test_data.gen_noise(fmin=10, amplitude=1)\n",
"# gen = rocof_test_data.gen_noise(fmin=60, amplitude=0.2)\n",
"# gen = rocof_test_data.test_harmonics()\n",
"# gen = rocof_test_data.gen_interharmonic(*rocof_test_data.test_interharmonics)\n",
"# gen = rocof_test_data.test_amplitude_steps()\n",
"# gen = rocof_test_data.test_amplitude_and_phase_steps()\n",
"test_data = []\n",
"test_labels = [ fun.__name__.replace('test_', '') for fun in rocof_test_data.all_tests ]\n",
"for gen in rocof_test_data.all_tests:\n",
" test_data.append(rocof_test_data.sample_waveform(gen(),\n",
" duration=duration,\n",
" sampling_rate=fs,\n",
" frequency=ff)[0])\n",
"# d = 10 # seconds\n",
"# test_data = np.sin(2*np.pi * ff * np.linspace(0, d, int(d*fs)))"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"spr_fmt = f'{fs}Hz' if fs<1000 else f'{fs/1e3:f}'.rstrip('.0') + 'kHz'\n",
"for label, data in zip(test_labels, test_data):\n",
" with open(f'rocof_test_data/rocof_test_{label}_{spr_fmt}.bin', 'wb') as f:\n",
" for sample in data:\n",
" f.write(struct.pack('<f', sample))"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"analysis_periods = 10\n",
"window_len = 256 # fs * analysis_periods/ff\n",
"nfft_factor = 1\n",
"sigma = window_len/8 # samples\n",
"quantization_bits = 14\n",
"\n",
"ffts = []\n",
"for item in test_data:\n",
" f, t, Zxx = signal.stft((item * (2**(quantization_bits-1) - 1)).round().astype(np.int16).astype(float),\n",
" fs = fs,\n",
" window=('gaussian', sigma),\n",
" nperseg = window_len,\n",
" nfft = window_len * nfft_factor)\n",
" #boundary = 'zeros')\n",
" ffts.append((f, t, Zxx))"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(129, 470)"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Zxx.shape"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"3.90625"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"1000/256"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "1bae03315efe4ed7a72b911fed0056ae",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, ax = plt.subplots(len(test_data), figsize=(8, 20), sharex=True)\n",
"fig.tight_layout(pad=2, h_pad=0.1)\n",
"\n",
"for fft, ax, label in zip(test_data, ax.flatten(), test_labels):\n",
" ax.plot((item * (2**(quantization_bits-1) - 1)).round())\n",
" \n",
" ax.set_title(label, pad=-20, color='white', bbox=dict(boxstyle=\"square\", ec=(0,0,0,0), fc=(0,0,0,0.8)))\n",
" ax.grid()\n",
" ax.set_ylabel('f [Hz]')\n",
"ax.set_xlabel('simulation time t [s]')\n",
"ax.set_xlim([5000, 5200])\n",
"None"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "7182190a6bbc4481a792d5f6b7d390a0",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, ax = plt.subplots(len(test_data), figsize=(8, 20), sharex=True)\n",
"fig.tight_layout(pad=2, h_pad=0.1)\n",
"\n",
"for fft, ax, label in zip(ffts, ax.flatten(), test_labels):\n",
" f, t, Zxx = fft\n",
" ax.pcolormesh(t[1:], f[:250], np.abs(Zxx[:250,1:]))\n",
" ax.set_title(label, pad=-20, color='white')\n",
" ax.grid()\n",
" ax.set_ylabel('f [Hz]')\n",
" ax.set_ylim([30, 75]) # Hz\n",
"ax.set_xlabel('simulation time t [s]')\n",
"None"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([ 0. , 3.90625, 7.8125 , 11.71875, 15.625 , 19.53125,\n",
" 23.4375 , 27.34375, 31.25 , 35.15625, 39.0625 , 42.96875,\n",
" 46.875 , 50.78125, 54.6875 , 58.59375, 62.5 , 66.40625,\n",
" 70.3125 , 74.21875, 78.125 , 82.03125, 85.9375 , 89.84375,\n",
" 93.75 , 97.65625, 101.5625 , 105.46875, 109.375 , 113.28125,\n",
" 117.1875 , 121.09375, 125. , 128.90625, 132.8125 , 136.71875,\n",
" 140.625 , 144.53125, 148.4375 , 152.34375, 156.25 , 160.15625,\n",
" 164.0625 , 167.96875, 171.875 , 175.78125, 179.6875 , 183.59375,\n",
" 187.5 , 191.40625, 195.3125 , 199.21875, 203.125 , 207.03125,\n",
" 210.9375 , 214.84375, 218.75 , 222.65625, 226.5625 , 230.46875,\n",
" 234.375 , 238.28125, 242.1875 , 246.09375, 250. , 253.90625,\n",
" 257.8125 , 261.71875, 265.625 , 269.53125, 273.4375 , 277.34375,\n",
" 281.25 , 285.15625, 289.0625 , 292.96875, 296.875 , 300.78125,\n",
" 304.6875 , 308.59375, 312.5 , 316.40625, 320.3125 , 324.21875,\n",
" 328.125 , 332.03125, 335.9375 , 339.84375, 343.75 , 347.65625,\n",
" 351.5625 , 355.46875, 359.375 , 363.28125, 367.1875 , 371.09375,\n",
" 375. , 378.90625, 382.8125 , 386.71875, 390.625 , 394.53125,\n",
" 398.4375 , 402.34375, 406.25 , 410.15625, 414.0625 , 417.96875,\n",
" 421.875 , 425.78125, 429.6875 , 433.59375, 437.5 , 441.40625,\n",
" 445.3125 , 449.21875, 453.125 , 457.03125, 460.9375 , 464.84375,\n",
" 468.75 , 472.65625, 476.5625 , 480.46875, 484.375 , 488.28125,\n",
" 492.1875 , 496.09375, 500. ])"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"f"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "d26336c7e27c44c7894919fdeb614891",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, axs = plt.subplots(len(test_data), figsize=(8, 20), sharex=True)\n",
"fig.tight_layout(pad=2.2, h_pad=0, w_pad=1)\n",
"\n",
"for fft, ax, label in zip(ffts, axs.flatten(), test_labels):\n",
" f, f_t, Zxx = fft\n",
" \n",
" n_f, n_t = Zxx.shape\n",
" # start, stop = 180, 220\n",
" # start, stop = 90, 110\n",
" # start, stop = 15, 35\n",
" # bounds_f = slice(start // 4 * nfft_factor, stop // 4 * nfft_factor)\n",
" f_min, f_max = 30, 70 # Hz\n",
" bounds_f = slice(np.argmax(f > f_min), np.argmin(f < f_max))\n",
" \n",
"\n",
" f_mean = np.zeros(Zxx.shape[1])\n",
" for t in range(1, Zxx.shape[1] - 1):\n",
" frame_f = f[bounds_f]\n",
" frame_step = frame_f[1] - frame_f[0]\n",
" time_step = f_t[1] - f_t[0]\n",
" #if t == 10:\n",
" # axs[-1].plot(frame_f, frame_Z)\n",
" frame_Z = np.abs(Zxx[bounds_f, t])\n",
" # frame_f = f[180:220]\n",
" # frame_Z = np.abs(Zxx[180:220, 40])\n",
" # frame_f = f[15:35]\n",
" # frame_Z = np.abs(Zxx[15:35, 40])\n",
" # plt.plot(frame_f, frame_Z)\n",
"\n",
" # peak_f = frame_f[np.argmax(frame)]\n",
" # plt.axvline(peak_f, color='red')\n",
"\n",
"# def gauss(x, *p):\n",
"# A, mu, sigma, o = p\n",
"# return A*np.exp(-(x-mu)**2/(2.*sigma**2)) + o\n",
" \n",
" def gauss(x, *p):\n",
" A, mu, sigma = p\n",
" return A*np.exp(-(x-mu)**2/(2.*sigma**2))\n",
" \n",
" f_start = frame_f[np.argmax(frame_Z)]\n",
" A_start = np.max(frame_Z)\n",
" p0 = [A_start, f_start, 1.]\n",
" try:\n",
" coeff, var = optimize.curve_fit(gauss, frame_f, frame_Z, p0=p0)\n",
" # plt.plot(frame_f, gauss(frame_f, *coeff))\n",
" #print(coeff)\n",
" A, mu, sigma, *_ = coeff\n",
" f_mean[t] = mu\n",
" except RuntimeError:\n",
" f_mean[t] = np.nan\n",
" ax.plot(f_t[1:-1], f_mean[1:-1])\n",
" \n",
"# b, a = signal.butter(3,\n",
"# 1/5, # Hz\n",
"# btype='lowpass',\n",
"# fs=1/time_step)\n",
"# filtered = signal.lfilter(b, a, f_mean[1:-1], axis=0)\n",
"# ax.plot(f_t[1:-1], filtered)\n",
" \n",
" ax.set_title(label, pad=-20)\n",
" ax.set_ylabel('f [Hz]')\n",
" ax.grid()\n",
" if not label in ['off_frequency', 'sweep_phase_steps']:\n",
" ax.set_ylim([49.90, 50.10])\n",
" var = np.var(f_mean[1:-1])\n",
" ax.text(0.5, 0.1, f'σ²={var * 1e3:.3g} mHz²', transform=ax.transAxes, ha='center')\n",
" ax.text(0.5, 0.25, f'σ={np.sqrt(var) * 1e3:.3g} mHz', transform=ax.transAxes, ha='center')\n",
"# ax.text(0.5, 0.2, f'filt. σ²={np.var(filtered) * 1e3:.3g} mHz', transform=ax.transAxes, ha='center')\n",
" else:\n",
" f_min, f_max = min(f_mean[1:-1]), max(f_mean[1:-1])\n",
" delta = f_max - f_min\n",
" ax.set_ylim(f_min - delta * 0.1, f_max + delta * 0.3)\n",
" \n",
"ax.set_xlabel('simulation time t [s]')\n",
"None"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "labenv",
"language": "python",
"name": "labenv"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.2"
}
},
"nbformat": 4,
"nbformat_minor": 4
}

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -13,6 +13,7 @@ all: safety_reset.pdf
safety_reset.pdf: resources/grid_freq_estimation.pdf
safety_reset.pdf: resources/gps_clock_jitter_analysis.pdf
safety_reset.pdf: resources/dsss_experiments-ber.pdf
safety_reset.pdf: resources/freq_meas_validation_rocof_testsuite.pdf
%.pdf: %.tex %.bib
pdflatex -shell-escape $<

View file

@ -1214,9 +1214,38 @@ with IO contention on the raspberry PI/linux side causing only 16 skipped sample
Captured raw waveform data is processed in the Jupyter Lab environment\cite{kluyver01} and grid frequency estimates are
extracted as described in sec. \ref{frequency_estimation} using the \textcite{gasior01} technique. Appendix
\ref{grid_freq_estimation_notebook} contains the Jupyter notebook we used for frequency measurement.
\ref{grid_freq_estimation_notebook} contains the Jupyter notebook we used for frequency measurement. In fig.\
\ref{freq_meas_feedback} we fed back to the frequency estimator its own output giving us an indication of its numerical
performance. The result was \SI{1.3}{\milli\hertz} of RMS noise over a \SI{3600}{\second} simulation time. This
indicates performance is good enough for our purposes. In addition to this we validated our algorithm's performance by
applying it to the test waveforms from \textcite{wright01}. In this test we got errors of \SI{4.4}{\milli\hertz} for the
\emph{noise} test waveform, \SI{0.027}{\milli\hertz} for the \emph{interharmonics} test waveform and
\SI{46}{\milli\hertz} for the \emph{amplitude and phase step} test waveform. Full results can be found in fig.\
\ref{freq_meas_rocof_reference}.
% TODO comparison against reference measurements?
\begin{figure}
\centering
\includegraphics[width=\textwidth]{../lab-windows/fig_out/freq_meas_feedback}
\caption{
The frequency estimation algorithm applied to a synthetic noise-less mains waveform generated from its own
output. This feedback simulation gives an indication of numerical errors in our estimation algorithm. The top
four graphs show a comparison of the original trace (blue) and the re-calculated trace (orange). The bottom
trace shows the difference between the two. As we can tell both traces agree very well with an overall RMS
deviation of about \SI{1.3}{\milli\hertz}. The bottom trace shows deviation growing over time. This is very
likely an effect of numerical errors in our ad-hoc waveform generator.
}
\label{freq_meas_feedback}
\end{figure}
\begin{figure}
\centering
\includegraphics[width=\textwidth]{../lab-windows/fig_out/freq_meas_rocof_reference}
\caption{
Performance of our frequency estimation algorithm against the test suite specified in \textcite{wright01}. Shown
are standard deviation and variance measurements as well as time-domain traces of differences.
}
\label{freq_meas_rocof_reference}
\end{figure}
\section{Channel simulation and parameter validation}
@ -1409,10 +1438,13 @@ indicates SER is related fairly monotonically to the signal-to-noise margins ins
\end{figure}
\section{Implementation of a demonstrator unit}
%FIXME
\section{Experimental results}
%FIXME
\section{Lessons learned}
%FIXME
@ -1444,6 +1476,7 @@ managing root keys for other government systems to also manage safety reset keys
of safety reset keys do not differ significantly from those for other types of root keys.
\section{Practical implementation}
%FIXME
\section{Zones of trust}
@ -1490,23 +1523,29 @@ microcontroller providing this type of virtualization on the one hand and the co
virtualization on the other hand. Virtualization systems such as TrustZone are still orders of magnitude more complex to
correctly configure than it is to simply use separate hardware and secure the interfaces in between.
\chapter{Alternative use of grid frequency modulation}
% FIXME random beacons? funky consensus protocols? proof of knowledge/cryptographic notary service?
\chapter{Conclusion}
%FIXME
\newpage
\appendix
\chapter{Acknowledgements}
%FIXME
\newpage
\chapter{References}
\nocite{*}
\nocite{*} % FIXME
\printbibliography
\newpage
\chapter{Transcripts of Jupyter notebooks used in this thesis}
\includenotebook{Grid frequency estimation}{grid_freq_estimation}
\includenotebook{Frequency sensor clock stability analysis}{gps_clock_jitter_analysis}
\includenotebook{DSSS modulation experiments}{dsss_experiments-ber}
%\includenotebook{Grid frequency estimation}{grid_freq_estimation}
%\includenotebook{Grid frequency estimation validation against ROCOF test suite}{freq_meas_validation_rocof_testsuite}
%\includenotebook{Frequency sensor clock stability analysis}{gps_clock_jitter_analysis}
%\includenotebook{DSSS modulation experiments}{dsss_experiments-ber}
\chapter{Demonstrator schematics and code}