sx172x: add library to communicate with semtech SX127x LoRa module

This commit is contained in:
King Kévin 2020-06-09 01:26:02 +02:00
parent 54456e0cb4
commit 6fa46ee86c
2 changed files with 306 additions and 0 deletions

145
lib/radio_sx172x.c Normal file
View File

@ -0,0 +1,145 @@
/** library to communication with Semtech SX172x LoRa radio module using SPI
* @file
* @author King Kévin <kingkevin@cuvoodoo.info>
* @copyright SPDX-License-Identifier: GPL-3.0-or-later
* @date 2020
* @note peripherals used: SPI @ref radio_sx172x_spi, GPIO @ref radio_sx172x_gpio
* @note the interrupts and corresponding DIO should be handled directly by the user
*/
/* standard libraries */
#include <stdint.h> // standard integer types
#include <stdlib.h> // general utilities
#include <stdbool.h> // boolean utilities
/* STM32 (including CM3) libraries */
#include <libopencmsis/core_cm3.h> // Cortex M3 utilities
#include <libopencm3/stm32/rcc.h> // real-time control clock library
#include <libopencm3/stm32/gpio.h> // general purpose input output library
#include <libopencm3/stm32/spi.h> // SPI library
/* own libraries */
#include "global.h" // common methods
#include "radio_sx172x.h" // own definitions
/** @defgroup radio_sx172x_spi SPI peripheral used to communicate with the SX172x
* @{
*/
#define RADIO_SX172X_SPI 2 /**< SPI peripheral */
/** @} */
/** @defgroup radio_sx172x_gpio GPIO used to control the SX172x
* @{
*/
#define RADIO_SX172X_GPIO_NRESET PB7 /**< reset input (active low) */
/** @} */
bool radio_sx172x_setup(void)
{
// setup SPI
rcc_periph_clock_enable(RCC_SPI_SCK_PORT(RADIO_SX172X_SPI)); // enable clock for GPIO peripheral for clock signal
gpio_set_mode(SPI_SCK_PORT(RADIO_SX172X_SPI), GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, SPI_SCK_PIN(RADIO_SX172X_SPI)); // set SCK as output (clock speed will be negotiated later)
rcc_periph_clock_enable(RCC_SPI_MOSI_PORT(RADIO_SX172X_SPI)); // enable clock for GPIO peripheral for MOSI signal
gpio_set_mode(SPI_MOSI_PORT(RADIO_SX172X_SPI), GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, SPI_MOSI_PIN(RADIO_SX172X_SPI)); // set MOSI as output
rcc_periph_clock_enable(RCC_SPI_MISO_PORT(RADIO_SX172X_SPI)); // enable clock for GPIO peripheral for MISO signal
gpio_set_mode(SPI_MISO_PORT(RADIO_SX172X_SPI), GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, SPI_MISO_PIN(RADIO_SX172X_SPI)); // set MISO as input
rcc_periph_clock_enable(RCC_SPI_NSS_PORT(RADIO_SX172X_SPI)); // enable clock for GPIO peripheral for NSS (CS) signal
gpio_set_mode(SPI_NSS_PORT(RADIO_SX172X_SPI), GPIO_MODE_OUTPUT_10_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, SPI_NSS_PIN(RADIO_SX172X_SPI)); // set NSS (CS) as output
rcc_periph_clock_enable(RCC_AFIO); // enable clock for SPI alternate function
rcc_periph_clock_enable(RCC_SPI(RADIO_SX172X_SPI)); // enable clock for SPI peripheral
spi_reset(SPI(RADIO_SX172X_SPI)); // clear SPI values to default
spi_init_master(SPI(RADIO_SX172X_SPI), SPI_CR1_BAUDRATE_FPCLK_DIV_8, SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE, SPI_CR1_CPHA_CLK_TRANSITION_2, SPI_CR1_DFF_8BIT, SPI_CR1_MSBFIRST); // initialise SPI as master, divide clock by 8 (72E6/8 < 10 MHz, max SX172X SCK is 10 MHz, maximum SPI PCLK clock is 72 MHz, depending on which SPI is used), set clock polarity to idle low (not that important), set clock phase to do bit change on falling edge (polarity depends on clock phase), use 8-bit frames , use MSb first
spi_set_full_duplex_mode(SPI(RADIO_SX172X_SPI)); // ensure we are in full duplex mode
spi_enable_software_slave_management(SPI(RADIO_SX172X_SPI)); // control NSS (CS) manually
spi_set_nss_high(SPI(RADIO_SX172X_SPI)); // set NSS high (internally) so we can output
spi_disable_ss_output(SPI(RADIO_SX172X_SPI)); // disable NSS output since we control CS manually
gpio_set(SPI_NSS_PORT(RADIO_SX172X_SPI), SPI_NSS_PIN(RADIO_SX172X_SPI)); // set CS high to unselect device
spi_enable(SPI(RADIO_SX172X_SPI)); // enable SPI
// use NRESET pin to read ensure it started
rcc_periph_clock_enable(GPIO_RCC(RADIO_SX172X_GPIO_NRESET)); // enable clock for GPIO port
gpio_set_mode(GPIO_PORT(RADIO_SX172X_GPIO_NRESET), GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO_PIN(RADIO_SX172X_GPIO_NRESET)); // set GPIO as input
sleep_ms(10); // wait for POR sequence to complete
return (0 != gpio_get(GPIO_PORT(RADIO_SX172X_GPIO_NRESET), GPIO_PIN(RADIO_SX172X_GPIO_NRESET))); // ensure the module has started
}
void radio_sx172x_release(void)
{
spi_disable(SPI(RADIO_SX172X_SPI));
rcc_periph_clock_disable(RCC_SPI(RADIO_SX172X_SPI));
gpio_set_mode(SPI_NSS_PORT(RADIO_SX172X_SPI), GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, SPI_NSS_PIN(RADIO_SX172X_SPI));
gpio_set_mode(SPI_MISO_PORT(RADIO_SX172X_SPI), GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, SPI_MISO_PIN(RADIO_SX172X_SPI));
gpio_set_mode(SPI_MOSI_PORT(RADIO_SX172X_SPI), GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, SPI_MOSI_PIN(RADIO_SX172X_SPI));
gpio_set_mode(SPI_SCK_PORT(RADIO_SX172X_SPI), GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, SPI_SCK_PIN(RADIO_SX172X_SPI));
}
bool radio_sx172x_reset(void)
{
gpio_clear(GPIO_PORT(RADIO_SX172X_GPIO_NRESET), GPIO_PIN(RADIO_SX172X_GPIO_NRESET)); // set low to reset device
gpio_set_mode(GPIO_PORT(RADIO_SX172X_GPIO_NRESET), GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO_PIN(RADIO_SX172X_GPIO_NRESET)); // set GPIO as output
sleep_us(200); // set low for at least 100 us (see datasheet section 7.2.2)
gpio_set_mode(GPIO_PORT(RADIO_SX172X_GPIO_NRESET), GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO_PIN(RADIO_SX172X_GPIO_NRESET)); // set GPIO as input
sleep_ms(6); // wait for at least 5 ms for reset to complete (see datasheet section 7.2.2)
return (0 != gpio_get(GPIO_PORT(RADIO_SX172X_GPIO_NRESET), GPIO_PIN(RADIO_SX172X_GPIO_NRESET))); // ensure the module has started
}
uint8_t radio_sx172x_read_register(enum radio_sx172x_register_t name)
{
gpio_clear(SPI_NSS_PORT(RADIO_SX172X_SPI), SPI_NSS_PIN(RADIO_SX172X_SPI)); // set CS low to select device
(void)SPI_DR(SPI(RADIO_SX172X_SPI)); // clear RXNE flag (by reading previously received data)
spi_send(SPI(RADIO_SX172X_SPI), name & 0x7f); // send register address (with 7th bit at 0 to read) and any data
spi_read(SPI(RADIO_SX172X_SPI)); // wait for data to be transferred, but discard the received data (clears RXNE go get the next data correctly)
spi_send(SPI(RADIO_SX172X_SPI), 0); // write any data so we can read back data
const uint8_t value = spi_read(SPI(RADIO_SX172X_SPI)); // read data
while ((SPI_SR(SPI(RADIO_SX172X_SPI)) & SPI_SR_BSY)); // wait for SPI to not be busy (communication completed)
gpio_set(SPI_NSS_PORT(RADIO_SX172X_SPI), SPI_NSS_PIN(RADIO_SX172X_SPI)); // set CS high to unselect device
return value;
}
void radio_sx172x_write_register(enum radio_sx172x_register_t name, uint8_t value)
{
gpio_clear(SPI_NSS_PORT(RADIO_SX172X_SPI), SPI_NSS_PIN(RADIO_SX172X_SPI)); // set CS low to select device
spi_send(SPI(RADIO_SX172X_SPI), name | 0x80); // send register address (with 7th bit at 1 to write) and value
spi_send(SPI(RADIO_SX172X_SPI), value); // write value
while ((SPI_SR(SPI(RADIO_SX172X_SPI)) & SPI_SR_BSY)); // wait for SPI to not be busy (communication completed)
gpio_set(SPI_NSS_PORT(RADIO_SX172X_SPI), SPI_NSS_PIN(RADIO_SX172X_SPI)); // set CS high to unselect device
}
void radio_sx172x_read_fifo(uint8_t addr, uint8_t* data, uint8_t length)
{
if (NULL == data || 0 == length || addr + length > 256) { // sanity check
return;
}
const uint8_t mode = radio_sx172x_read_register(RADIO_SX172X_REG_OP_MODE); // read in which mode we are
if (0 == (mode & 0x7)) { // we are in sleep mode
radio_sx172x_write_register(RADIO_SX172X_REG_OP_MODE, (mode & 0xf8) | 1); // go to standby mode so we can access the FIFO registers
}
radio_sx172x_write_register(RADIO_SX172X_REG_LORA_FIFO_ADDR_PTR, addr); // set address to be read
gpio_clear(SPI_NSS_PORT(RADIO_SX172X_SPI), SPI_NSS_PIN(RADIO_SX172X_SPI)); // set CS low to select device
(void)SPI_DR(SPI(RADIO_SX172X_SPI)); // clear RXNE flag (by reading previously received data)
spi_send(SPI(RADIO_SX172X_SPI), RADIO_SX172X_REG_FIFO & 0x7f); // select FIFO register (with 7th bit at 0 to read)
spi_read(SPI(RADIO_SX172X_SPI)); // wait for data to be transferred, but discard the received data (clears RXNE go get the next data correctly)
for (uint8_t i = 0; i < length; i++) {
spi_send(SPI(RADIO_SX172X_SPI), 0); // write any data so we can read back data
data[i] = spi_read(SPI(RADIO_SX172X_SPI)); // read data
}
while ((SPI_SR(SPI(RADIO_SX172X_SPI)) & SPI_SR_BSY)); // wait for SPI to not be busy (communication completed)
gpio_set(SPI_NSS_PORT(RADIO_SX172X_SPI), SPI_NSS_PIN(RADIO_SX172X_SPI)); // set CS high to unselect device
}
void radio_sx172x_write_fifo(uint8_t addr, const uint8_t* data, uint8_t length)
{
if (NULL == data || 0 == length || addr + length > 256) { // sanity check
return;
}
const uint8_t mode = radio_sx172x_read_register(RADIO_SX172X_REG_OP_MODE); // read in which mode we are
if (0 == (mode & 0x7)) { // we are in sleep mode
radio_sx172x_write_register(RADIO_SX172X_REG_OP_MODE, (mode & 0xf8) | 1); // go to standby mode so we can access the FIFO registers
}
radio_sx172x_write_register(RADIO_SX172X_REG_LORA_FIFO_ADDR_PTR, addr); // set address to be read
gpio_clear(SPI_NSS_PORT(RADIO_SX172X_SPI), SPI_NSS_PIN(RADIO_SX172X_SPI)); // set CS low to select device
spi_send(SPI(RADIO_SX172X_SPI), RADIO_SX172X_REG_FIFO | 0x80); // select FIFO register (with 7th bit at 1 to write)
for (uint8_t i = 0; i < length; i++) {
spi_send(SPI(RADIO_SX172X_SPI), data[i]); // write data
}
while ((SPI_SR(SPI(RADIO_SX172X_SPI)) & SPI_SR_BSY)); // wait for SPI to not be busy (communication completed)
gpio_set(SPI_NSS_PORT(RADIO_SX172X_SPI), SPI_NSS_PIN(RADIO_SX172X_SPI)); // set CS high to unselect device
}

161
lib/radio_sx172x.h Normal file
View File

@ -0,0 +1,161 @@
/** library to communication with Semtech SX172x LoRa radio module using SPI
* @file
* @author King Kévin <kingkevin@cuvoodoo.info>
* @copyright SPDX-License-Identifier: GPL-3.0-or-later
* @date 2020
* @note peripherals used: SPI @ref radio_sx172x_spi, GPIO @ref radio_sx172x_gpio
* @note the interrupts and corresponding DIO should be handled directly by the user
*/
#pragma once
/** register addresses */
enum radio_sx172x_register_t {
RADIO_SX172X_REG_FIFO = 0x00,
RADIO_SX172X_REG_OP_MODE = 0x01,
RADIO_SX172X_REG_FSKOOK_BITRATE_MSB = 0x02,
RADIO_SX172X_REG_FSKOOK_BITRATE_LSB = 0x03,
RADIO_SX172X_REG_FSKOOK_FDEV_MSB = 0x04,
RADIO_SX172X_REG_FSKOOK_FDEV_LSB = 0x05,
RADIO_SX172X_REG_FRF_MSB = 0x06,
RADIO_SX172X_REG_FRF_MID = 0x07,
RADIO_SX172X_REG_FRF_LSB = 0x08,
RADIO_SX172X_REG_PA_CONFIG = 0x09,
RADIO_SX172X_REG_PA_RAMP = 0x0A,
RADIO_SX172X_REG_OCP = 0x0B,
RADIO_SX172X_REG_LNA = 0x0C,
RADIO_SX172X_REG_FSKOOB_RX_CONFIG = 0x0D,
RADIO_SX172X_REG_LORA_FIFO_ADDR_PTR = 0x0D,
RADIO_SX172X_REG_FSKOOB_RSSI_CONFIG = 0x0E,
RADIO_SX172X_REG_LORA_FIFO_TX_BASE_ADDR = 0x0E,
RADIO_SX172X_REG_FSKOOB_RSSI_COLLISION = 0x0F,
RADIO_SX172X_REG_LORA_FIFO_RX_BASE_ADDR = 0x0F,
RADIO_SX172X_REG_FSKOOB_RSSI_THRESH = 0x10,
RADIO_SX172X_REG_LORA_FIFO_RX_CURRENT_ADDR = 0x10,
RADIO_SX172X_REG_FSKOOK_RSSI_VALUE = 0x11,
RADIO_SX172X_REG_LORA_IRQ_FLAGS_MASK = 0x11,
RADIO_SX172X_REG_FSKOOK_RX_BW = 0x12,
RADIO_SX172X_REG_LORA_IRQ_FLAGS = 0x12,
RADIO_SX172X_REG_FSKOOK_AFC_BW = 0x13,
RADIO_SX172X_REG_LORA_RX_NB_BYTES = 0x13,
RADIO_SX172X_REG_FSKOOK_OOK_PEAK = 0x14,
RADIO_SX172X_REG_LORA_RX_HEADER_CNT_VALUE_MSB = 0x14,
RADIO_SX172X_REG_FSKOOK_OOK_FIX = 0x15,
RADIO_SX172X_REG_LORA_RX_HEADER_CNT_VALUE_LSB = 0x15,
RADIO_SX172X_REG_FSKOOK_OOK_AVG = 0x16,
RADIO_SX172X_REG_LORA_RX_PACKET_CNT_VALUE_MSB = 0x16,
RADIO_SX172X_REG_LORA_RX_PACKET_CNT_VALUE_LSB = 0x17,
RADIO_SX172X_REG_LORA_MODEM_STAT = 0x18,
RADIO_SX172X_REG_LORA_PKT_SNR_VALUE = 0x19,
RADIO_SX172X_REG_FSKOOK_AFC_FEI = 0x1A,
RADIO_SX172X_REG_LORA_PKT_RSSI_VALUE = 0x1A,
RADIO_SX172X_REG_FSKOOK_AFC_MSB = 0x1B,
RADIO_SX172X_REG_LORA_RSSI_VALUE = 0x1B,
RADIO_SX172X_REG_FSKOOK_AFC_LSB = 0x1C,
RADIO_SX172X_REG_LORA_HOP_CHANNEL = 0x1C,
RADIO_SX172X_REG_FSKOOK_FEI_MSB = 0x1D,
RADIO_SX172X_REG_LORA_MODEM_CONFIG_1 = 0x1D,
RADIO_SX172X_REG_FSKOOK_FEI_LSB = 0x1E,
RADIO_SX172X_REG_LORA_MODEM_CONFIG_2 = 0x1E,
RADIO_SX172X_REG_FSKOOK_PREAMBLE_DETECT = 0x1F,
RADIO_SX172X_REG_LORA_SYMB_TIMEOUT_LSB = 0x1F,
RADIO_SX172X_REG_FSKOOK_RX_TIMEOUT_1 = 0x20,
RADIO_SX172X_REG_LORA_PREAMBLE_MSB = 0x20,
RADIO_SX172X_REG_FSKOOK_RX_TIMEOUT_2 = 0x21,
RADIO_SX172X_REG_LORA_PREAMBLE_LSB = 0x21,
RADIO_SX172X_REG_FSKOOK_RX_TIMEOUT_3 = 0x22,
RADIO_SX172X_REG_LORA_PAYLOAD_LENGTH = 0x22,
RADIO_SX172X_REG_FSKOOK_RX_DELAY = 0x23,
RADIO_SX172X_REG_LORA_MAX_PAYLOD_LENGTH = 0x23,
RADIO_SX172X_REG_FSKOOK_OSC = 0x24,
RADIO_SX172X_REG_LORA_HOP_PERIOD = 0x24,
RADIO_SX172X_REG_FSKOOK_PREAMBLE_MSB = 0x25,
RADIO_SX172X_REG_LORA_FIFO_RX_BYTE_ADDR = 0x25,
RADIO_SX172X_REG_FSKOOK_PREAMBLE_LSB = 0x26,
RADIO_SX172X_REG_LORA_MODEM_CONFIG_3 = 0x26,
RADIO_SX172X_REG_FSKOOK_SYNC_CONFIG = 0x27,
RADIO_SX172X_REG_FSKOOK_SYNC_VALUE_1 = 0x28,
RADIO_SX172X_REG_LORA_FEI_MSB = 0x28,
RADIO_SX172X_REG_FSKOOK_SYNC_VALUE_2 = 0x29,
RADIO_SX172X_REG_LORA_FEI_MID = 0x29,
RADIO_SX172X_REG_FSKOOK_SYNC_VALUE_3 = 0x2A,
RADIO_SX172X_REG_LORA_FEI_LSB = 0x2A,
RADIO_SX172X_REG_FSKOOK_SYNC_VALUE_4 = 0x2B,
RADIO_SX172X_REG_FSKOOK_SYNC_VALUE_5 = 0x2C,
RADIO_SX172X_REG_LORA_RSSI_WIDEBAND = 0x2C,
RADIO_SX172X_REG_FSKOOK_SYNC_VALUE_6 = 0x2D,
RADIO_SX172X_REG_FSKOOK_SYNC_VALUE_7 = 0x2E,
RADIO_SX172X_REG_FSKOOK_SYNC_VALUE_8 = 0x2F,
RADIO_SX172X_REG_LORA_IF_FREQ_1 = 0x2F,
RADIO_SX172X_REG_FSKOOK_PACKET_CONFIG_1 = 0x30,
RADIO_SX172X_REG_LORA_IF_FREQ_2 = 0x30,
RADIO_SX172X_REG_FSKOOK_PACKET_CONFIG_2 = 0x31,
RADIO_SX172X_REG_LORA_DETECT_OPTIMIZE = 0x31,
RADIO_SX172X_REG_FSKOOK_PAYLOAD_LENGTH = 0x32,
RADIO_SX172X_REG_FSKOOK_NODE_ADRS = 0x33,
RADIO_SX172X_REG_LORA_INVERT_IQ = 0x33,
RADIO_SX172X_REG_FSKOOK_BROADCAST_ADRS = 0x34,
RADIO_SX172X_REG_FSKOOK_FIFO_THRESH = 0x35,
RADIO_SX172X_REG_FSKOOK_SEQ_CONFIG_1 = 0x36,
RADIO_SX172X_REG_LORA_HIGH_BW_OPTIMIZE_1 = 0x36,
RADIO_SX172X_REG_FSKOOK_SEQ_CONFIG_2 = 0x37,
RADIO_SX172X_REG_LORA_DETECTION_THRESHOLD = 0x37,
RADIO_SX172X_REG_FSKOOK_TIMER_RESOLV = 0x38,
RADIO_SX172X_REG_FSKOOK_TIMER_1_COEF = 0x39,
RADIO_SX172X_REG_LORA_SYNC_WORD = 0x39,
RADIO_SX172X_REG_FSKOOK_TIMER_2_COEF = 0x3A,
RADIO_SX172X_REG_LORA_HIGH_BW_OPTIMIZE_2 = 0x3A,
RADIO_SX172X_REG_FSKOOK_IMAGE_CAL = 0x3B,
RADIO_SX172X_REG_LORA_INVERT_IQ_2 = 0x3B,
RADIO_SX172X_REG_FSKOOK_TEMP = 0x3C,
RADIO_SX172X_REG_FSKOOK_LOW_BAT = 0x3D,
RADIO_SX172X_REG_FSKOOK_IRQ_FLAGS_1 = 0x3E,
RADIO_SX172X_REG_FSKOOK_IRQ_FLAGS_2 = 0x3F,
RADIO_SX172X_REG_DIO_MAPPING_1 = 0x40,
RADIO_SX172X_REG_DIO_MAPPING_2 = 0x41,
RADIO_SX172X_REG_VERSION = 0x42,
RADIO_SX172X_REG_FSKOOK_PLL_HOP = 0x44,
RADIO_SX172X_REG_TCX0 = 0x4B,
RADIO_SX172X_REG_PA_DAC = 0x4D,
RADIO_SX172X_REG_FORMER_TEMP = 0x5B,
RADIO_SX172X_REG_FSKOOK_BIT_RATE_FRAC = 0x5D,
RADIO_SX172X_REG_AGC_REF = 0x61,
RADIO_SX172X_REG_AGC_THRESH_1 = 0x62,
RADIO_SX172X_REG_AGC_THRESH_2 = 0x63,
RADIO_SX172X_REG_AGC_THRESH_3 = 0x64,
RADIO_SX172X_REG_PLL = 0x70,
};
/** setup communication to SX172x
* @return if SX172x is ready
*/
bool radio_sx172x_setup(void);
/** release resources and peripherals used by communication */
void radio_sx172x_release(void);
/** reset SX172x chip
* @return if SX172x is ready
*/
bool radio_sx172x_reset(void);
/** read register from SX172x
* @param[in] name register name to read
* @return register value read
*/
uint8_t radio_sx172x_read_register(enum radio_sx172x_register_t name);
/** write register to SX172x
* @param[in] name register name to write
* @param[in} value register value to write
*/
void radio_sx172x_write_register(enum radio_sx172x_register_t name, uint8_t value);
/** read FIFO data
* @param[in] addr address of the data to be read in the FIFO buffer
* @param[out] data buffer to store read data
* @param[in] length number of byte to read
* @warning puts device in standby mode when in sleep mode
*/
void radio_sx172x_read_fifo(uint8_t addr, uint8_t* data, uint8_t length);
/** write FIFO data
* @param[in] addr address of the data to be written in the FIFO buffer
* @param[out] data data to be written in FIFO buffer
* @param[in] length number of byte to written
* @warning puts device in standby mode when in sleep mode
*/
void radio_sx172x_write_fifo(uint8_t addr, const uint8_t* data, uint8_t length);