diff --git a/stmhal/i2c.c b/stmhal/i2c.c
index d94a99ee9e661d9739fcf311c8cb6dee92cbb718..b6e3d9beee0a4fec84c7051dd0b4a19b8661810a 100644
--- a/stmhal/i2c.c
+++ b/stmhal/i2c.c
@@ -14,6 +14,51 @@
 #include "bufhelper.h"
 #include "i2c.h"
 
+// Usage model:
+//
+// I2C objects are created attached to a specific bus.  They can be initialised
+// when created, or initialised later on:
+//
+//     from pyb import I2C
+//
+//     i2c = I2C(1)                         # create on bus 1
+//     i2c = I2C(1, I2C.MASTER)             # create and init as a master
+//     i2c.deinit()                         # turn off the peripheral
+//     i2c.init(I2C.MASTER, baudrate=20000) # init as a master
+//     i2c.init(I2C.SLAVE, addr=0x42)       # init as a slave with given address
+//
+// Printing the i2c object gives you information about its configuration.
+//
+// Basic methods for slave are send and recv:
+//
+//     i2c.send('abc')      # send 3 bytes
+//     i2c.send(0x42)       # send a single byte, given by the number
+//     data = i2c.recv(3)   # receive 3 bytes
+//
+// To receive inplace, first create a bytearray:
+//
+//     data = bytearray(3)  # create a buffer
+//     i2c.recv(data)       # receive 3 bytes, writing them into data
+//
+// You can specify a timeout (in ms):
+//
+//     i2c.send(b'123', timeout=2000)   # timout after 2 seconds
+//
+// A master must specify the recipient's address:
+//
+//     i2c.init(I2C.MASTER)
+//     i2c.send('123', 0x42)        # send 3 bytes to slave with address 0x42
+//     i2c.send(b'456', addr=0x42)  # keyword for address
+//
+// Master also has other methods:
+//
+//     i2c.is_ready(0x42)           # check if slave 0x42 is ready
+//     i2c.scan()                   # scan for slaves on the bus, returning
+//                                  #   a list of valid addresses
+//     i2c.mem_read(3, 0x42, 2)     # read 3 bytes from memory of slave 0x42,
+//                                  #   starting at address 2 in the slave
+//     i2c.mem_write('abc', 0x42, 2, timeout=1000)
+
 #define PYB_I2C_MASTER (0)
 #define PYB_I2C_SLAVE  (1)
 
diff --git a/stmhal/spi.c b/stmhal/spi.c
index 352c594c9e63ee32e2886c2a65237ba21421fdf4..99bc08c3b10971caf3b634fe3d5c913d6ea1215d 100644
--- a/stmhal/spi.c
+++ b/stmhal/spi.c
@@ -14,6 +14,25 @@
 #include "bufhelper.h"
 #include "spi.h"
 
+// Usage model:
+//
+// See usage model of I2C in i2c.c.  SPI is very similar.  Main difference is
+// parameters to init the SPI bus:
+//
+//     from pyb import SPI
+//     spi = SPI(1, SPI.MASTER, baudrate=600000, polarity=1, phase=1, crc=0x7)
+//
+// Only required parameter is mode, SPI.MASTER or SPI.SLAVE.  Polarity can be
+// 0 or 1, and is the level the idle clock line sits at.  Phase can be 1 or 2
+// for number of edges.  Crc can be None for no CRC, or a polynomial specifier.
+//
+// Additional method for SPI:
+//
+//     data = spi.send_recv(b'1234')        # send 4 bytes and receive 4 bytes
+//     buf = bytearray(4)
+//     spi.send_recv(b'1234', buf)          # send 4 bytes and receive 4 into buf
+//     spi.send_recv(buf, buf)              # send/recv 4 bytes from/to buf
+
 #if MICROPY_HW_ENABLE_SPI1
 SPI_HandleTypeDef SPIHandle1 = {.Instance = NULL};
 #endif
@@ -384,6 +403,9 @@ STATIC mp_obj_t pyb_spi_send_recv(uint n_args, const mp_obj_t *args, mp_map_t *k
         } else {
             // recv argument given
             mp_get_buffer_raise(vals[1].u_obj, &bufinfo_recv, MP_BUFFER_WRITE);
+            if (bufinfo_recv.len != bufinfo_send.len) {
+                nlr_raise(mp_obj_new_exception_msg(&mp_type_ValueError, "recv must be same length as send"));
+            }
             o_ret = MP_OBJ_NULL;
         }
     }
diff --git a/stmhal/usart.c b/stmhal/usart.c
index c737955ac2b8ac701b54d96c12e6c2028343e903..4e5502aa35827b1f61115214219507beafc45f14 100644
--- a/stmhal/usart.c
+++ b/stmhal/usart.c
@@ -12,6 +12,21 @@
 #include "bufhelper.h"
 #include "usart.h"
 
+// Usage model:
+//
+// See usage model of I2C in i2c.c.  USART is very similar.  Main difference is
+// parameters to init the USART bus:
+//
+//     from pyb import USART
+//     usart = USART(1, 9600)                        # init with given baudrate
+//     usart.init(9600, bits=8, stop=1, parity=None) # init with given parameters
+//
+// Bits can be 8 or 9, stop can be 1 or 2, parity can be None, 0 (even), 1 (odd).
+//
+// Extra method:
+//
+//     usart.any()              # returns True if any characters waiting
+
 struct _pyb_usart_obj_t {
     mp_obj_base_t base;
     pyb_usart_t usart_id;