diff --git a/examples/bme280/README.md b/examples/bme280/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4325a1eb730bc29c97d3da3c6d5c7d9b1e71f539
--- /dev/null
+++ b/examples/bme280/README.md
@@ -0,0 +1,16 @@
+# BME280 sensor example
+
+Pinout is according to the C source: MOSI - 23, MISO - 19, CLK - 18, CS = 5:
+
+```c
+  struct spi bme280 = {.mosi = 23, .miso = 19, .clk = 18, .cs = {5, -1, -1}};
+```
+
+Build, flash and monitor:
+
+```
+Chip ID: 96, expecting 96
+Temp: 23.69
+Chip ID: 96, expecting 96
+Temp: 25.5
+```
diff --git a/examples/bme280/main.c b/examples/bme280/main.c
index 0206332919d26320f6ed543187860051c9b34aeb..25e11cba0f3ddddeb4ed1c4d54be354cd7c9f919 100644
--- a/examples/bme280/main.c
+++ b/examples/bme280/main.c
@@ -1,16 +1,62 @@
 #include <sdk.h>
 
-static int led = 2, pin = 4;
+static unsigned readn(struct spi *spi, uint8_t reg, int n) {
+  unsigned rx = 0;
+  spi_begin(spi, 0);
+  spi_txn(spi, reg | 0x80);
+  for (int i = 0; i < n; i++) rx <<= 8, rx |= spi_txn(spi, 0);
+  spi_end(spi, 0);
+  return rx;
+}
+
+static void write8(struct spi *spi, uint8_t reg, uint8_t val) {
+  spi_begin(spi, 0);
+  spi_txn(spi, (uint8_t)(reg & ~0x80));
+  spi_txn(spi, val);
+  spi_end(spi, 0);
+}
+
+uint16_t swap16(uint16_t val) {
+  uint8_t data[2] = {0, 0};
+  memcpy(&data, &val, sizeof(data));
+  return (uint16_t)((uint16_t) data[1] | (((uint16_t) data[0]) << 8));
+}
+
+// Taken from the BME280 datasheet, 4.2.3
+int32_t read_temp(struct spi *spi) {
+  int32_t t = (int32_t)(readn(spi, 0xfa, 3) >> 4);  // Read temperature reg
+
+  uint16_t c1 = (uint16_t) readn(spi, 0x88, 2);  // dig_T1
+  uint16_t c2 = (uint16_t) readn(spi, 0x8a, 2);  // dig_T2
+  uint16_t c3 = (uint16_t) readn(spi, 0x8c, 2);  // dig_T3
+
+  int32_t t1 = (int32_t) swap16(c1);
+  int32_t t2 = (int32_t)(int16_t) swap16(c2);
+  int32_t t3 = (int32_t)(int16_t) swap16(c3);
+
+  int32_t var1 = ((((t >> 3) - (t1 << 1))) * t2) >> 11;
+  int32_t var2 = (((((t >> 4) - t1) * ((t >> 4) - t1)) >> 12) * t3) >> 14;
+
+  return ((var1 + var2) * 5 + 128) >> 8;
+}
 
 int main(void) {
   wdt_disable();
-  gpio_output(led);
-  gpio_input(pin);
+
+  struct spi bme280 = {.mosi = 23, .miso = 19, .clk = 18, .cs = {5, -1, -1}};
+  spi_init(&bme280);
+
+  write8(&bme280, 0xe0, 0xb6);          // Soft reset
+  spin(999999);                         // Wait until reset
+  write8(&bme280, 0xf4, 0);             // REG_CONTROL, MODE_SLEEP
+  write8(&bme280, 0xf5, (3 << 5));      // REG_CONFIG, filter = off, 20ms
+  write8(&bme280, 0xf4, (1 << 5) | 3);  // REG_CONFIG, MODE_NORMAL
 
   for (;;) {
-    sdk_log("in: %d\n", gpio_read(pin));
-    gpio_toggle(led);
-    spin(2999999);
+    sdk_log("Chip ID: %d, expecting 96\n", readn(&bme280, 0xd0, 1));
+    int temp = read_temp(&bme280);
+    sdk_log("Temp: %d.%d\n", temp / 100, temp % 100);
+    spin(9999999);
   }
 
   return 0;
diff --git a/include/gpio.h b/include/gpio.h
index bc5ab473bc98fcbaab0fa70a7a1fdd24d65631e0..f932665af84e3dd740ee9b223a3ff80c4529bb57 100644
--- a/include/gpio.h
+++ b/include/gpio.h
@@ -7,7 +7,7 @@
 #define GPIO_IN1_REG REG(0X3ff44040)               // Pins 32-39
 #define GPIO_ENABLE1_REG REG(0X3ff4402c)           // Pins 32-39
 
-static inline void gpio_output_enable(int pin, int enable) {
+static inline void gpio_output_enable(int pin, bool enable) {
   volatile unsigned long *r = GPIO_ENABLE_REG;
   if (pin > 31) pin -= 31, r = GPIO_ENABLE1_REG;
   r[0] &= ~BIT(pin);
@@ -19,7 +19,7 @@ static inline void gpio_output(int pin) {
   gpio_output_enable(pin, 1);
 }
 
-static inline void gpio_write(int pin, int value) {
+static inline void gpio_write(int pin, bool value) {
   volatile unsigned long *r = GPIO_OUT_REG;
   if (pin > 31) pin -= 31, r = GPIO_OUT1_REG;
   r[0] &= ~BIT(pin);               // Clear first
@@ -41,10 +41,10 @@ static inline void gpio_input(int pin) {
   volatile unsigned long *mux = REG(0X3ff49000);
   if (pin < 0 || pin > (int) sizeof(map) || map[pin] == 0) return;
   gpio_output_enable(pin, 0);  // Disable output
-  mux[pin] |= BIT(9);          // Enable input
+  mux[map[pin]] |= BIT(9);     // Enable input
 }
 
-static inline int gpio_read(int pin) {
+static inline bool gpio_read(int pin) {
   volatile unsigned long *r = GPIO_IN_REG;
   if (pin > 31) pin -= 31, r = GPIO_IN1_REG;
   return r[0] & BIT(pin) ? 1 : 0;
diff --git a/include/sdk.h b/include/sdk.h
index 990d11270cc5726b44c161e35c2b6f73a7cc3cf4..6b4e23f7b859919f5904db066ed93d48add41a86 100644
--- a/include/sdk.h
+++ b/include/sdk.h
@@ -6,6 +6,8 @@
 #include <assert.h>
 #include <errno.h>
 #include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
 #include <stdlib.h>
 #include <string.h>
 
diff --git a/include/spi.h b/include/spi.h
index fb6247da85fc9300835c056ed76e47489ba05b7c..f351d658d096b89ee76a95807b1cced86b69fc9b 100644
--- a/include/spi.h
+++ b/include/spi.h
@@ -1,6 +1,15 @@
 struct spi {
-  int miso, mosi, clk, freq;
+  int miso, mosi, clk, cs[3];
 };
 
-int spi_init(struct spi *spi);
-int spi_txn(struct spi *spi);
+bool spi_init(struct spi *spi);
+unsigned char spi_txn(struct spi *spi, unsigned char tx);
+
+static inline void spi_begin(struct spi *spi, int cs) {
+  gpio_write(spi->cs[cs], 0);
+}
+
+static inline void spi_end(struct spi *spi, int cs) {
+  gpio_write(spi->cs[cs], 1);
+}
+
diff --git a/src/libc.c b/src/libc.c
index 54da7c071df8c29f516904d55cbe9cf059e297f4..d0bb4ea63b254f3dbd2689ae3ccf1d7382424c1f 100644
--- a/src/libc.c
+++ b/src/libc.c
@@ -45,6 +45,12 @@ void *memset(void *dest, int c, size_t n) {
   return dest;
 }
 
+void *memcpy(void *dst, const void *src, size_t len) {
+  unsigned char *d = dst, *s = (unsigned char *) src;
+  for (size_t i = 0; i < len; i++) d[i] = s[i];
+  return dst;
+}
+
 char *strcpy(char *dst, const char *src) {
   for (size_t i = 0; src[i] != '\0'; i++) dst[i] = src[i];
   return dst;
diff --git a/src/spi.c b/src/spi.c
index 7ebf563e3f581604622a18d380c824d45ae715e8..d82f7269eea1e484dab92e522c7719f6ed318f37 100644
--- a/src/spi.c
+++ b/src/spi.c
@@ -3,12 +3,36 @@
 
 #include <sdk.h>
 
-int spi_init(struct spi *spi) {
-  (void) spi;
-  return 0;
+// TODO(cpq): make this configurable with accurate frequency
+static inline void spi_clock_delay(void) {
+  spin(9);
 }
 
-int spi_txn(struct spi *spi) {
-  (void) spi;
-  return 0;
+bool spi_init(struct spi *spi) {
+  if (spi->miso < 0 || spi->mosi < 0 || spi->clk < 0) return false;
+  gpio_input(spi->miso);
+  gpio_output(spi->mosi);
+  gpio_output(spi->clk);
+  for (size_t i = 0; i < sizeof(spi->cs) / sizeof(spi->cs[0]); i++) {
+    if (spi->cs[i] < 0) continue;
+    gpio_output(spi->cs[i]);
+    gpio_write(spi->cs[i], 1);
+  }
+  return true;
+}
+
+// Send a byte, and return a received byte
+unsigned char spi_txn(struct spi *spi, unsigned char tx) {
+  unsigned char rx = 0;
+  for (int i = 0; i < 8; i++) {
+    gpio_write(spi->mosi, tx & 0x80);   // Set mosi
+    spi_clock_delay();                  // Wait half cycle
+    gpio_write(spi->clk, 1);            // Clock high
+    rx = (unsigned char) (rx << 1);     // "rx <<= 1" gives warning??
+    if (gpio_read(spi->miso)) rx |= 1;  // Read mosi
+    spi_clock_delay();                  // Wait half cycle
+    gpio_write(spi->clk, 0);            // Clock low
+    tx = (unsigned char) (tx << 1);     // Again, avoid warning
+  }
+  return rx;  // Return the received byte
 }