diff --git a/Makefile b/Makefile
index 2f46237258fb558d645fc6b3b229c8879d041b15..9e5a4670930bca65c061076616538595c5fbdfd6 100644
--- a/Makefile
+++ b/Makefile
@@ -2,8 +2,10 @@ TOOLCHAIN_PREFIX =
 CC = $(TOOLCHAIN_PREFIX)gcc
 OBJDUMP = $(TOOLCHAIN_PREFIX)objdump
 
+TARGETS = libaes.so test_aes.elf cache_profiling_hit.elf cache_profiling_pp1.elf cache_profiling_pp8.elf
+
 .PHONY: all clean
-all: libaes.so test_aes.elf cache_profiling_hit.elf
+all: $(TARGETS)
 
 libaes.so: aes/aes_core.c aes/aes_local.h aes/aes.h
 	$(CC) -fPIC aes/aes_core.c -Iaes -shared -o $@
@@ -14,5 +16,11 @@ test_aes.elf: src/test_aes.c libaes.so
 cache_profiling_hit.elf: src/cache_profiling_hit.c cache/cache_util.c cache/cache_util.h cache/cache_low.h visual/histogram.c visual/histogram.h
 	$(CC) src/cache_profiling_hit.c cache/cache_util.c visual/histogram.c -Icache -Ivisual -o $@
 
+cache_profiling_pp1.elf: src/cache_profiling_pp1.c cache/cache_util.c cache/cache_util.h cache/cache_low.h visual/histogram.c visual/histogram.h
+	$(CC) src/cache_profiling_pp1.c cache/cache_util.c visual/histogram.c -Icache -Ivisual -o $@
+
+cache_profiling_pp8.elf: src/cache_profiling_pp8.c cache/cache_l1.c cache/cache_l1.h cache/cache_util.c cache/cache_util.h cache/cache_low.h visual/histogram.c visual/histogram.h
+	$(CC) src/cache_profiling_pp8.c cache/cache_l1.c cache/cache_util.c visual/histogram.c -Icache -Ivisual -o $@
+
 clean:
 	rm -f -- *.elf *.so *.dump *.csv
diff --git a/cache/cache_l1.c b/cache/cache_l1.c
new file mode 100644
index 0000000000000000000000000000000000000000..65085d57b9dd93c81549887847e68995bc25d657
--- /dev/null
+++ b/cache/cache_l1.c
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2016 CSIRO
+ *
+ * This file is part of Mastik.
+ *
+ * Mastik is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Mastik is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Mastik.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <sys/mman.h>
+#include <assert.h>
+#include <strings.h>
+
+#include "cache_low.h"
+#include "cache_l1.h"
+
+#define L1_ASSOCIATIVITY 8
+
+#define L1_CACHELINE 64
+#define L1_STRIDE (L1_CACHELINE * L1_SETS)
+
+#define PTR(set, way, ptr) (void *)(((uintptr_t)l1->memory) + ((set) * L1_CACHELINE) + ((way) * L1_STRIDE) + ((ptr)*sizeof(void *)))
+#define LNEXT(p) (*(void **)(p))
+
+struct l1pp{
+  void *memory;
+  void *fwdlist;
+  void *bkwlist;
+  uint8_t monitored[L1_SETS];
+  int nsets;
+};
+
+static void rebuild(l1pp_t l1) {
+  if (l1->nsets == 0) {
+    l1->fwdlist = l1->bkwlist = NULL;
+    return;
+  }
+  for (int i = 0; i < l1->nsets - 1; i++) {
+    LNEXT(PTR(l1->monitored[i], L1_ASSOCIATIVITY - 1, 0)) = PTR(l1->monitored[i+1], 0, 0);
+    LNEXT(PTR(l1->monitored[i], 0, 1)) = PTR(l1->monitored[i+1], L1_ASSOCIATIVITY - 1, 1);
+  }
+  l1->fwdlist = LNEXT(PTR(l1->monitored[l1->nsets - 1], L1_ASSOCIATIVITY - 1, 0)) = PTR(l1->monitored[0], 0, 0);
+  l1->bkwlist = LNEXT(PTR(l1->monitored[l1->nsets - 1], 0, 1)) = PTR(l1->monitored[0], L1_ASSOCIATIVITY - 1, 1);
+}
+
+static void probelist(void *p, int segments, int seglen, uint16_t *results) {
+  while (segments--) {
+    uint32_t s = rdtscp();
+    for (int i = seglen; i--; ) {
+      asm volatile (""::"r" (p):);
+      p = LNEXT(p);
+    }
+    uint32_t res = rdtscp() - s;
+    *results = res > UINT16_MAX ? UINT16_MAX : res;
+    results++;
+  }
+}
+
+l1pp_t l1_prepare(void) {
+  l1pp_t l1 = (l1pp_t)malloc(sizeof(struct l1pp));
+  l1->memory = mmap(0, PAGE_SIZE * L1_ASSOCIATIVITY, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0);
+  l1->fwdlist = NULL;
+  l1->bkwlist = NULL;
+  for (int set = 0; set < L1_SETS; set++) {
+    for (int way = 0; way < L1_ASSOCIATIVITY - 1; way++) {
+      LNEXT(PTR(set, way, 0)) = PTR(set, way+1, 0);
+      LNEXT(PTR(set, way+1, 1)) = PTR(set, way, 1);
+    }
+  }
+//  l1_monitorall(l1);
+  return l1;
+}
+
+void l1_release(l1pp_t l1) {
+  munmap(l1->memory, PAGE_SIZE * L1_ASSOCIATIVITY);
+  bzero(l1, sizeof(struct l1pp));
+  free(l1);
+}
+
+int l1_monitor(l1pp_t l1, int line) {
+  for (int i = 0;  i < l1->nsets; i++)
+    if (l1->monitored[i] == line)
+      return 0;
+  l1->monitored[l1->nsets++] = line;
+  rebuild(l1);
+  return 1;
+}
+
+int l1_unmonitor(l1pp_t l1, int line) {
+  for (int i = 0;  i < l1->nsets; i++)
+    if (l1->monitored[i] == line) {
+      l1->monitored[i] = l1->monitored[l1->nsets--];
+      rebuild(l1);
+      return 1;
+    }
+  return 0;
+}
+
+void l1_monitorall(l1pp_t l1) {
+  for (int i = 0; i < L1_SETS; i++)
+    l1->monitored[i] = i;
+  l1->nsets = L1_SETS;
+  l1_randomise(l1);
+}
+
+void l1_unmonitorall(l1pp_t l1) {
+  l1->nsets = 0;
+  rebuild(l1);
+}
+
+int l1_getmonitoredset(l1pp_t l1, int *lines, int nlines) {
+  if (lines != NULL) {
+    if (nlines > l1->nsets)
+      nlines = l1->nsets;
+    for (int i = 0; i < nlines; i++)
+      lines[i] = l1->monitored[i];
+  }
+  return l1->nsets;
+}
+
+int l1_nsets(l1pp_t l1) {
+  return l1->nsets;
+}
+
+void l1_randomise(l1pp_t l1) {
+  for (int i = 0; i < l1->nsets; i++) {
+    int p = random() % (l1->nsets - i) + i;
+    uint8_t t = l1->monitored[p];
+    l1->monitored[p] = l1->monitored[i];
+    l1->monitored[i] = t;
+  }
+  rebuild(l1);
+}
+
+void l1_probe(l1pp_t l1, uint16_t *results) {
+  probelist(l1->fwdlist, l1->nsets, L1_ASSOCIATIVITY, results);
+}
+
+void l1_bprobe(l1pp_t l1, uint16_t *results) {
+  probelist(l1->bkwlist, l1->nsets, L1_ASSOCIATIVITY, results);
+}
+
+
+int l1_repeatedprobe(l1pp_t l1, int nrecords, uint16_t *results, int slot) {
+  assert(l1 != NULL);
+  assert(results != NULL);
+
+  if (nrecords == 0)
+    return 0;
+
+  int len = l1->nsets;
+
+  for (int i = 0; i < nrecords; /* Increment inside */) {
+    l1_probe(l1, results);
+    results += len;
+    i++;
+    l1_bprobe(l1, results);
+    results += len;
+    i++;
+  }
+  return nrecords;
+}
+
diff --git a/cache/cache_l1.h b/cache/cache_l1.h
new file mode 100644
index 0000000000000000000000000000000000000000..8cf2cea54dff8b1b1b5b461cf1bbbc53809e5105
--- /dev/null
+++ b/cache/cache_l1.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016 CSIRO
+ *
+ * This file is part of Mastik.
+ *
+ * Mastik is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Mastik is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Mastik.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __CACHE_L1_H__
+#define __CACHE_L1_H__ 1
+
+#define L1_SETS 64
+
+typedef struct l1pp *l1pp_t;
+
+l1pp_t l1_prepare(void);
+void l1_release(l1pp_t l1);
+
+int l1_monitor(l1pp_t l1, int line);
+int l1_unmonitor(l1pp_t l1, int line);
+void l1_monitorall(l1pp_t l1);
+void l1_unmonitorall(l1pp_t l1);
+int l1_getmonitoredset(l1pp_t l1, int *lines, int nlines);
+void l1_randomise(l1pp_t l1);
+
+void l1_probe(l1pp_t l1, uint16_t *results);
+void l1_bprobe(l1pp_t l1, uint16_t *results);
+
+// Slot is currently not implemented
+int l1_repeatedprobe(l1pp_t l1, int nrecords, uint16_t *results, int slot);
+
+
+
+#endif // __CACHE_L1_H__
+
diff --git a/cache/cache_low.h b/cache/cache_low.h
index 9537c8d31fb7f569b22821f4b661dd92834880b8..93f19dcf1cdf754c908a08eb58970e378a04255d 100644
--- a/cache/cache_low.h
+++ b/cache/cache_low.h
@@ -3,6 +3,12 @@
 
 #include <stdint.h>
 
+#ifdef PAGE_SIZE
+#undef PAGE_SIZE
+#endif
+#define PAGE_SIZE 4096
+//sysconf(_SC_PAGE_SIZE)
+
 static inline int memaccess(void *v) {
   int rv;
 #ifndef __riscv
diff --git a/src/cache_profiling_hit.c b/src/cache_profiling_hit.c
index cffc9b94c932557cb8bffa4c6c20da4a394c623e..b3bf424e9b4aef1d6381a8481d62e4fa84059e77 100644
--- a/src/cache_profiling_hit.c
+++ b/src/cache_profiling_hit.c
@@ -45,6 +45,7 @@ int main(void) {
   }
 
   fclose(fcache_profiling);
+  unmap_offset(addr);
 
   return 0;
 }
diff --git a/src/cache_profiling_pp1.c b/src/cache_profiling_pp1.c
new file mode 100644
index 0000000000000000000000000000000000000000..f381d63252e783a2757ab814fcc9f9a55f3d55d9
--- /dev/null
+++ b/src/cache_profiling_pp1.c
@@ -0,0 +1,75 @@
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdlib.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "cache_low.h"
+#include "cache_util.h"
+#include "histogram.h"
+
+#define SAMPLES 10000
+#define MAX_CYCLES 500
+
+#define L1_SETS 64
+#define L1_CACHELINE 64
+
+int main(int argc, char **argv) {
+
+  if (argc != 2) {
+    printf("usage %s <set to monitor(0-%d)>, set%d refer to all sets\n", argv[0], L1_SETS, L1_SETS);
+    return 1;
+  }
+
+  int set_to_monitor = atoi(argv[1]);
+  if (set_to_monitor < 0 || set_to_monitor > L1_SETS) {
+    printf("usage %s <set to monitor(0-%d)>, set%d refer to all sets\n", argv[0], L1_SETS, L1_SETS);
+    return 1;
+  }
+
+  uint16_t cache_access_cycles[SAMPLES];
+  uint16_t cache_access_histogram[MAX_CYCLES];
+
+  memset(cache_access_histogram, 0, MAX_CYCLES*sizeof(uint16_t));
+
+  void* addr = map_offset("libaes.so", 0);
+  printf("Monitored addr:   %p\n", addr);
+
+  // Prepare Prime+Probe playground
+  void* pp_memory = mmap(0, PAGE_SIZE * 1, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0);
+  void* pp_memory_access = pp_memory + set_to_monitor*L1_CACHELINE;
+  printf("Prime+Probe addr: %p\n", pp_memory_access);
+
+  // Cache access cycles
+  for (int i=0; i < SAMPLES; i++){
+    memaccesstime_u16(pp_memory_access);
+    cache_access_cycles[i] = memaccesstime_u16(addr);
+  }
+
+  // Cache histogram
+  for (int i=0; i < SAMPLES; i++) {
+    if (cache_access_cycles[i] < MAX_CYCLES) {
+      cache_access_histogram[cache_access_cycles[i]] ++;
+    } else {
+      cache_access_histogram[MAX_CYCLES] ++;
+    }
+  }
+
+  // Print histogram
+  draw_histogram_u16(cache_access_histogram, MAX_CYCLES);
+
+  // Export to csv file
+  FILE* fcache_profiling = fopen("cache_profiling_pp1.csv","w");
+  fprintf(fcache_profiling,"index;cache access\n");
+
+  for(int i = 0; i < MAX_CYCLES; i++) {
+    fprintf(fcache_profiling,"%d;%d;\n", i, cache_access_histogram[i]);
+  }
+
+  fclose(fcache_profiling);
+  unmap_offset(addr);
+  munmap(pp_memory, PAGE_SIZE * 1);
+
+  return 0;
+}
diff --git a/src/cache_profiling_pp8.c b/src/cache_profiling_pp8.c
new file mode 100644
index 0000000000000000000000000000000000000000..dbf948f1c25ae35859a260ae3a020c06fdaf22b3
--- /dev/null
+++ b/src/cache_profiling_pp8.c
@@ -0,0 +1,97 @@
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "cache_low.h"
+#include "cache_util.h"
+#include "cache_l1.h"
+#include "histogram.h"
+
+#define SAMPLES 10000
+#define MAX_CYCLES 500
+
+int main(int argc, char **argv) {
+
+  if (argc != 2) {
+    printf("usage %s <set to monitor(0-%d)>, set%d refer to all sets\n", argv[0], L1_SETS, L1_SETS);
+    return 1;
+  }
+
+  int set_to_monitor = atoi(argv[1]);
+  if (set_to_monitor < 0 || set_to_monitor > L1_SETS) {
+    printf("usage %s <set to monitor(0-%d)>, set%d refer to all sets\n", argv[0], L1_SETS, L1_SETS);
+    return 1;
+  }
+
+  uint16_t cache_access_cycles[SAMPLES];
+  uint16_t cache_access_histogram[MAX_CYCLES];
+
+  memset(cache_access_histogram, 0, MAX_CYCLES*sizeof(uint16_t));
+
+  void* addr = map_offset("libaes.so",0);
+  printf("Monitored addr:  %p\n", addr);
+
+  // Prepare Prime+Probe playground
+  l1pp_t l1 = l1_prepare();
+  if (set_to_monitor < L1_SETS) {
+    // Monitor 1 set
+    l1_monitor(l1, set_to_monitor);
+  } else {
+    l1_monitorall(l1);
+  }
+  int nsets = l1_getmonitoredset(l1, NULL, 0);
+  printf("L1 monitored set: %d\n", nsets);
+
+  uint16_t pp_results[nsets*SAMPLES];
+  uint16_t pp_histogram[MAX_CYCLES];
+
+  // Cache access cycles
+  for (int i=0; i < SAMPLES; i++){
+    l1_probe(l1, &pp_results[i*nsets]);
+    cache_access_cycles[i] = memaccesstime_u16(addr);
+  }
+
+  // Cache histogram
+  printf("Cache access histogram:\n");
+  for (int i=0; i < SAMPLES; i++) {
+    if (cache_access_cycles[i] < MAX_CYCLES) {
+      cache_access_histogram[cache_access_cycles[i]] ++;
+    } else {
+      cache_access_histogram[MAX_CYCLES] ++;
+    }
+  }
+
+  // Print histogram
+  draw_histogram_u16(cache_access_histogram, MAX_CYCLES);
+
+  // Prime+Probe histogram
+  printf("Prime+Probe histogram\n");
+  for (int i=0; i < SAMPLES; i++) {
+    for (int j=0; j < nsets; j++) {
+      uint16_t res = pp_results[i*nsets + j];
+      if (res < MAX_CYCLES) {
+        pp_histogram[res] ++;
+      } else {
+        pp_histogram[MAX_CYCLES] ++;
+      }
+    }
+  }
+
+  // Print histogram
+  draw_histogram_u16(pp_histogram, MAX_CYCLES);
+
+  // Export to csv file
+  FILE* fcache_profiling = fopen("cache_profiling_pp8.csv","w");
+  fprintf(fcache_profiling,"index;cache access\n");
+
+  for(int i = 0; i < MAX_CYCLES; i++) {
+    fprintf(fcache_profiling,"%d;%d;\n", i, cache_access_histogram[i]);
+  }
+
+  fclose(fcache_profiling);
+  unmap_offset(addr);
+  l1_release(l1);
+
+  return 0;
+}