diff --git a/arm/spe.c b/arm/spe.c
index 7996f79..2f5ee35 100644
--- a/arm/spe.c
+++ b/arm/spe.c
@@ -19,6 +19,7 @@
 #include "alloc_page.h"
 #include <bitops.h>
 #include "alloc.h"
+#include <asm/gic.h>
 
 struct spe {
 	int min_interval;
@@ -36,13 +37,37 @@ struct spe {
 	bool unique_record_size;
 };
 
+enum spe_event_exception_class {
+	EC_STAGE1_DATA_ABORT =  0x24,
+	EC_STAGE2_DATA_ABORT = 0x25,
+	EC_OTHER = 0,
+};
+
+struct spe_event {
+	enum spe_event_exception_class ec;
+	bool dl;	/* data lost */
+	bool ea;	/* external abort */
+	bool s;		/* service */
+	bool coll;	/* collision */
+	union {
+		bool buffer_filled; /* ec = other */
+	} mss;
+};
+
 static struct spe spe;
 
+struct spe_stats {
+	struct spe_event observed;
+	bool unexpected;
+};
+static struct spe_stats spe_stats;
+
 #ifdef __arm__
 
 static bool spe_probe(void) { return false; }
 static void test_spe_introspection(void) { }
 static void test_spe_buffer(void) { }
+static void test_spe_events(void) { }
 
 #else
 
@@ -95,6 +120,16 @@ static void test_spe_buffer(void) { }
 #define PMSCR_EL1_TS			0x20
 #define PMSCR_EL1_PCT			0x40
 
+#define PMBSR_EL1_COLL			0x10000
+#define PMBSR_EL1_S			0x20000
+#define PMBSR_EL1_EA			0x40000
+#define PMBSR_EL1_DL			0x80000
+#define PMBSR_EL1_EC_SHIFT		26
+#define PMBSR_EL1_EC_MASK		0x3F
+#define PMBSR_EL1_MISS_MASK		0xFFFF
+
+#define SPE_PPI 21
+
 static int min_interval(uint8_t idr_bits)
 {
 	switch (idr_bits) {
@@ -119,6 +154,44 @@ static int min_interval(uint8_t idr_bits)
 	}
 }
 
+static int decode_syndrome_register(uint64_t sr, struct spe_event *event, bool verbose)
+{
+	if (!sr)
+		return 0;
+
+	if (sr & PMBSR_EL1_S)
+		event->s = true;
+	if (sr & PMBSR_EL1_COLL)
+		event->coll = true;
+	if (sr & PMBSR_EL1_EA)
+		event->ea = true;
+	if (sr & PMBSR_EL1_DL)
+		event->dl = true;
+	if (verbose)
+		report_info("PMBSR_EL1: Service=%d Collision=%d External Fault=%d DataLost=%d",
+			    event->s, event->coll, event->ea, event->dl);
+
+	switch ((sr >> PMBSR_EL1_EC_SHIFT) & PMBSR_EL1_EC_MASK) {
+	case EC_OTHER:
+		event->ec = EC_OTHER;
+		event->mss.buffer_filled = sr & 0x1;
+		if (verbose)
+			report_info("PMBSR_EL1: EC = OTHER buffer filled=%d", event->mss.buffer_filled);
+		break;
+	case EC_STAGE1_DATA_ABORT:
+		event->ec = EC_STAGE1_DATA_ABORT;
+		report_info("PMBSR_EL1: EC = stage 1 data abort");
+		break;
+	case EC_STAGE2_DATA_ABORT:
+		event->ec = EC_STAGE2_DATA_ABORT;
+		report_info("PMBSR_EL1: EC = stage 2 data abort");
+		break;
+	default:
+		return -1;
+	}
+	return 0;
+}
+
 static bool spe_probe(void)
 {
 	uint64_t pmbidr_el1, pmsidr_el1;
@@ -224,6 +297,13 @@ static void reset(void)
 
 	/* Make sure the syndrome register is void */
 	write_sysreg_s(0, PMBSR_EL1);
+
+	memset(&spe_stats, 0, sizeof(spe_stats));
+}
+
+inline bool event_match(struct spe_event *observed, struct spe_event *expected)
+{
+	return !memcmp(observed, expected, sizeof(struct spe_event));
 }
 
 static inline void drain(void)
@@ -235,6 +315,7 @@ static inline void drain(void)
 
 static void test_spe_buffer(void)
 {
+	struct spe_event observed = {}, expected = {};
 	uint64_t pmbsr_el1, val1, val2;
 	void *addr = malloc(10 * PAGE_SIZE);
 
@@ -290,7 +371,61 @@ static void test_spe_buffer(void)
 		report_info("This corresponds to %ld record(s) of %d bytes",
 			    val2 / spe.maxsize, spe.maxsize);
 	pmbsr_el1 = read_sysreg_s(PMBSR_EL1);
-	report(!pmbsr_el1, "PMBSR_EL1: no event");
+	report(!(decode_syndrome_register(pmbsr_el1, &observed, true)) &&
+	       event_match(&observed, &expected), "PMBSR_EL1: no event");
+
+	free(addr);
+}
+
+static void irq_handler(struct pt_regs *regs)
+{
+	uint32_t irqstat, irqnr;
+
+	irqstat = gic_read_iar();
+	irqnr = gic_iar_irqnr(irqstat);
+
+	if (irqnr == SPE_PPI) {
+		uint64_t pmbsr_el1 = read_sysreg_s(PMBSR_EL1);
+
+		if (decode_syndrome_register(pmbsr_el1, &spe_stats.observed, true))
+			spe_stats.unexpected = true;
+		report_info("SPE IRQ! SR=0x%lx", pmbsr_el1);
+		write_sysreg_s(0, PMBSR_EL1);
+	} else {
+		spe_stats.unexpected = true;
+	}
+	gic_write_eoir(irqstat);
+}
+
+static inline bool has_event_occurred(struct spe_event *expected)
+{
+	return (!spe_stats.unexpected && event_match(&spe_stats.observed, expected));
+}
+
+static void test_spe_events(void)
+{
+	struct spe_event expected = {.ec = EC_OTHER, .mss.buffer_filled = true, .s = true};
+	void *addr = malloc(10 * PAGE_SIZE);
+
+	gic_enable_defaults();
+	install_irq_handler(EL1H_IRQ, irq_handler);
+	local_irq_enable();
+	gic_enable_irq(SPE_PPI);
+
+	reset();
+
+	/* Willingly set pmblimitr tp pmdptr */
+	spe.pmblimitr_el1 = spe.pmbptr_el1;
+
+	mem_access_loop(addr, 100000, spe.pmblimitr_el1 | PMBLIMITR_EL1_E);
+	drain();
+	report(has_event_occurred(&expected), "PMBSR_EL1: buffer full event");
+
+	/* redo it once */
+
+	mem_access_loop(addr, 100000, spe.pmblimitr_el1 | PMBLIMITR_EL1_E);
+	drain();
+	report(has_event_occurred(&expected), "PMBSR_EL1: buffer full event");
 
 	free(addr);
 }
@@ -317,6 +452,10 @@ int main(int argc, char *argv[])
 		report_prefix_push(argv[1]);
 		test_spe_buffer();
 		report_prefix_pop();
+	} else if (strcmp(argv[1], "spe-events") == 0) {
+		report_prefix_push(argv[1]);
+		test_spe_events();
+		report_prefix_pop();
 	} else {
 		report_abort("Unknown sub-test '%s'", argv[1]);
 	}
diff --git a/arm/unittests.cfg b/arm/unittests.cfg
index bb0e84c..b2b07be 100644
--- a/arm/unittests.cfg
+++ b/arm/unittests.cfg
@@ -150,6 +150,14 @@ extra_params = -append 'spe-buffer'
 accel = kvm
 arch = arm64
 
+[spe-events]
+file = spe.flat
+groups = spe
+arch = arm64
+extra_params = -append 'spe-events'
+accel = kvm
+arch = arm64
+
 # Test GIC emulation
 [gicv2-ipi]
 file = gic.flat
