Skip to content

Instantly share code, notes, and snippets.

@szczys
Created November 22, 2023 20:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save szczys/638a8db4802d79a6c96f33f6b54a37c3 to your computer and use it in GitHub Desktop.
Save szczys/638a8db4802d79a6c96f33f6b54a37c3 to your computer and use it in GitHub Desktop.
Golioth Supercon 2023 badge hack to add i2c peripheral mode to Micropython (and add data chunking features)
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <hardware/i2c.h>
#include "../micropython/lib/pico-sdk/src/rp2_common/pico_i2c_slave/include/pico/i2c_slave.h"
//#include "led_ctrl.h"
#include "pico/stdlib.h"
#include "i2c_fifo.h"
#include "supercon_data_transfer.h"
uint8_t _semver[3] = { 1, 2, 3 };
#define REG_SEMVER 0x42
#define REG_HASDATA 0xE0
#define REG_GETDATA 0xE1
#define OSTENTUS_ADDR 0x12
#define OSTENTUS_I2C_PORT i2c1
#define OSTENTUS_SDA_PIN 14
#define OSTENTUS_SCL_PIN 15
#define OSTENTUS_BAUDRATE 100000 // 100 kHz
uint8_t buffer[I2C_BUFFER_SIZE] = {0};
#define FIFO_SIZE 32
#define FIFO_DATA_OFFSET 2 // cmd addr, data_len
#define FIFO_DATA_SIZE I2C_BUFFER_SIZE-FIFO_DATA_OFFSET
typedef struct {
uint8_t reg;
uint8_t len;
char data[I2C_BUFFER_SIZE-FIFO_DATA_OFFSET];
} i2c_msg_t;
typedef enum {
I2C_IDLE,
I2C_RECEIVE,
I2C_REQUEST,
I2C_HAS_REGISTER
} fifo_state_t;
typedef struct {
fifo_state_t state;
uint8_t head;
uint8_t tail;
uint8_t has_data;
uint8_t idx;
i2c_msg_t fifo[FIFO_SIZE];
} fifo_status_t;
fifo_status_t ctx = {
.state = I2C_IDLE,
.head = 0,
.tail = 0,
.has_data = 0,
.idx = 0
};
static void reset_ctx(void) {
ctx.state = I2C_IDLE;
ctx.head = 0;
ctx.tail = 0;
ctx.has_data = 0;
ctx.idx = 0;
}
static i2c_msg_t *get_head(void) {
return &ctx.fifo[ctx.head];
}
static i2c_msg_t *get_tail(void) {
return &ctx.fifo[ctx.tail];
}
static void store_byte(uint8_t data) {
if ((ctx.head == ctx.tail) && ctx.has_data) {
//Buffer is full, do nothing.
}
if (ctx.state == I2C_IDLE) {
get_tail()->reg = data;
ctx.state = I2C_RECEIVE;
} else {
get_tail()->data[ctx.idx] = data;
++ctx.idx;
if (ctx.idx >= FIFO_DATA_SIZE) {
ctx.idx = FIFO_DATA_OFFSET - 1;
}
}
}
static uint8_t read_byte(void) {
switch(get_tail()->reg) {
case REG_SEMVER:
if (ctx.idx < sizeof(_semver)) {
return _semver[ctx.idx++];
} else {
return 0xff;
}
break;
case REG_HASDATA:
return (uint8_t)data_available();
break;
case REG_GETDATA:
if (ctx.idx >= I2C_PACKET_SIZE) {
return 0xff;
} else {
return get_data();
}
break;
default:
return 0xff;
}
}
static void stop_byte(void)
{
if (ctx.state == I2C_REQUEST) {
ctx.state = I2C_IDLE;
ctx.idx = 0;
return;
}
if (ctx.state == I2C_RECEIVE) {
if (ctx.idx == 0) {
// Got register address for upcoming read
ctx.state = I2C_HAS_REGISTER;
return;
}
i2c_msg_t *buf = get_tail();
// Write data len
buf->len = ctx.idx;
switch(buf->reg) {
// Handle LED change directly (don't save to fifo)
/*
case ADDR_POWER:
if (buf->len) {
led_push_single(LED_POWER, buf->data[0]);
}
break;
case ADDR_BATTERY:
if (buf->len) {
led_push_single(LED_BATTERY, buf->data[0]);
}
break;
case ADDR_INTERNET:
if (buf->len) {
led_push_single(LED_INTERNET, buf->data[0]);
}
break;
case ADDR_GOLIOTH:
if (buf->len) {
led_push_single(LED_GOLIOTH, buf->data[0]);
}
break;
case ADDR_USER:
if (buf->len) {
led_push_single(LED_USER, buf->data[0]);
}
break;
case ADDR_BITMASK:
if (buf->len) {
led_push_mask(buf->data[0]);
}
break;
*/
// Everything else goes into the fifo
default:
// Indicate there's info in the fifo
if (++ctx.has_data > FIFO_SIZE) {
ctx.has_data = FIFO_SIZE;
};
if (++ctx.tail >= FIFO_SIZE) {
ctx.tail = 0;
}
}
//Reset to defaults
ctx.state = I2C_IDLE;
ctx.idx = 0;
}
}
static void i2c_responder_handler(i2c_inst_t *i2c, i2c_slave_event_t event)
{
switch (event) {
case I2C_SLAVE_RECEIVE: // master has written some data
store_byte(i2c_read_byte_raw(i2c));
break;
case I2C_SLAVE_REQUEST: // master is requesting data
// load from memory
ctx.state = I2C_REQUEST;
i2c_write_byte_raw(i2c, read_byte());
break;
case I2C_SLAVE_FINISH: // master has signalled Stop / Restart
stop_byte();
break;
default:
break;
}
}
int fifo_pop(uint8_t * r_buf) {
if (ctx.has_data == 0) {
return -1;
}
r_buf[0] = get_head()->reg;
r_buf[1] = get_head()->len;
memcpy(r_buf+2, get_head()->data, get_head()->len);
++ctx.head;
if (ctx.head >= FIFO_SIZE) {
ctx.head = 0;
}
--ctx.has_data;
return 0;
}
bool fifo_has_data(void) {
if (ctx.has_data) {
return true;
}
return false;
}
void fifo_init(void)
{
reset_ctx();
//led_init();
gpio_init(OSTENTUS_SDA_PIN);
gpio_set_function(OSTENTUS_SDA_PIN, GPIO_FUNC_I2C);
gpio_pull_up(OSTENTUS_SDA_PIN);
gpio_init(OSTENTUS_SCL_PIN);
gpio_set_function(OSTENTUS_SCL_PIN, GPIO_FUNC_I2C);
gpio_pull_up(OSTENTUS_SCL_PIN);
i2c_init(OSTENTUS_I2C_PORT, OSTENTUS_BAUDRATE);
// configure I2C0 for slave mode
i2c_slave_init(OSTENTUS_I2C_PORT, OSTENTUS_ADDR, &i2c_responder_handler);
}
#include <stdint.h>
#define I2C_BUFFER_SIZE 64
void fifo_init(void);
int fifo_pop(uint8_t * r_buf);
bool fifo_has_data(void);
# Create an INTERFACE library for our C module.
add_library(usermod_ostentus_i2c INTERFACE)
# Add our source files to the lib
target_sources(usermod_ostentus_i2c INTERFACE
${CMAKE_CURRENT_LIST_DIR}/i2c_fifo.c
${CMAKE_CURRENT_LIST_DIR}/ostentus_i2c.c
${CMAKE_CURRENT_LIST_DIR}/supercon_data_cache.c
${CMAKE_CURRENT_LIST_DIR}/supercon_data_transfer.c
)
# Add the current directory as an include directory.
target_include_directories(usermod_ostentus_i2c INTERFACE
${CMAKE_CURRENT_LIST_DIR}
)
# Link our INTERFACE library to the usermod target.
target_link_libraries(usermod INTERFACE usermod_ostentus_i2c)
EXAMPLE_MOD_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(EXAMPLE_MOD_DIR)/ostentus_i2c.c
# We can add our module folder to include paths if needed
# This is not actually needed in this example.
CFLAGS_USERMOD += -I$(EXAMPLE_MOD_DIR)
OSTENTUS_I2C_MOD_DIR := $(USERMOD_DIR)
// include micropython api.
#include "py/runtime.h"
#include "i2c_fifo.h"
#include "supercon_data_cache.h"
#include "supercon_data_transfer.h"
static mp_obj_t init(void) {
fifo_init();
return mp_const_none;
}
mp_define_const_fun_obj_0(init_obj, init);
static mp_obj_t fifo_finalize_samples(void) {
finalize_samples();
return mp_const_none;
}
mp_define_const_fun_obj_0(fifo_finalize_samples_obj, fifo_finalize_samples);
static mp_obj_t fifo_put_point(mp_obj_t x_obj, mp_obj_t y_obj) {
mp_int_t x = mp_obj_get_int(x_obj);
mp_int_t y = mp_obj_get_int(y_obj);
put_point(x, y);
return mp_const_none;
}
mp_define_const_fun_obj_2(fifo_put_point_obj, fifo_put_point);
static mp_obj_t fifo_init_samples(void) {
init_samples("philip j. fry", 50);
return mp_const_none;
}
mp_define_const_fun_obj_0(fifo_init_samples_obj, fifo_init_samples);
static mp_obj_t outgoing_data_available(void) {
return mp_obj_new_bool(data_available());
}
mp_define_const_fun_obj_0(outgoing_data_available_obj, outgoing_data_available);
static mp_obj_t has_data(void) {
return mp_obj_new_bool(fifo_has_data());
}
mp_define_const_fun_obj_0(has_data_obj, has_data);
static mp_obj_t pop(void) {
// your code here!
uint8_t f_buf[64];
fifo_pop(f_buf);
// signature: mp_obj_t mp_obj_new_bytes(const byte* data, size_t len);
return mp_obj_new_bytes((const unsigned char *)f_buf, f_buf[1]+2);
}
mp_define_const_fun_obj_0(pop_obj, pop);
// define all properties of the module.
// table entries are key/value pairs of the attribute name (a string)
// and the micropython object reference.
// all identifiers and strings are written as mp_qstr_xxx and will be
// optimized to word-sized integers by the build system (interned strings).
static const mp_rom_map_elem_t example_module_globals_table[] = {
{ mp_rom_qstr(mp_qstr___name__), mp_rom_qstr(mp_qstr_ostentus_i2c) },
{ mp_rom_qstr(mp_qstr_init), mp_rom_ptr(&init_obj) },
{ mp_rom_qstr(mp_qstr_fifo_finalize_samples), mp_rom_ptr(&fifo_finalize_samples_obj) },
{ mp_rom_qstr(mp_qstr_fifo_put_point), mp_rom_ptr(&fifo_put_point_obj) },
{ mp_rom_qstr(mp_qstr_fifo_init_samples), mp_rom_ptr(&fifo_init_samples_obj) },
{ mp_rom_qstr(mp_qstr_outgoing_data_available), mp_rom_ptr(&outgoing_data_available_obj) },
{ mp_rom_qstr(mp_qstr_pop), mp_rom_ptr(&pop_obj) },
{ mp_rom_qstr(mp_qstr_has_data), mp_rom_ptr(&has_data_obj) },
};
static mp_define_const_dict(example_module_globals, example_module_globals_table);
// define module object.
const mp_obj_module_t ostentus_i2c_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t *)&example_module_globals,
};
// register the module to make it available in python.
mp_register_module(mp_qstr_ostentus_i2c, ostentus_i2c_cmodule);
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include "../micropython/lib/pico-sdk/src/rp2_common/pico_rand/include/pico/rand.h"
#include "supercon_data_cache.h"
#include "supercon_data_transfer.h"
struct context {
uint16_t point_idx;
} cache_ctx;
Samples _samples;
SuperconMeta _meta;
void finalize_samples(void) {
stage_data(&_meta, &_samples, cache_ctx.point_idx);
sample_cache_set_ready(true);
}
void init_samples(char *name, uint16_t interval_us) {
cache_ctx.point_idx = 0;
snprintf(_meta.header.name, sizeof(_meta.header.name), "%s", name);
_meta.header.interval_us = interval_us;
_meta.header.uid = (uint16_t)get_rand_32();
}
int put_point(uint8_t x, uint8_t y) {
if (sample_cache_is_ready()) {
/* Buffer is alreay staged for Alduel */
return -1;
}
Point p = {
.x = x,
.y = y,
};
_samples.points[cache_ctx.point_idx] = p;
if (cache_ctx.point_idx++ >= MAX_SAMPLES) {
finalize_samples();
}
return 0;
}
#ifndef SUPERCON_DATA_CACHE_H
#define SUPERCON_DATA_CACHE_H
#include <stdint.h>
//FIXME: This should be dynamically allocated
#define MAX_SAMPLES 32768 // Samples are 2 bytes each
typedef struct {
uint8_t x;
uint8_t y;
} Point;
typedef union {
Point points[MAX_SAMPLES];
uint8_t bytes[MAX_SAMPLES * 2];
} Samples;
void finalize_samples(void);
void init_samples(char *name, uint16_t interval_us);
int put_point(uint8_t x, uint8_t y);
#endif
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include "supercon_data_cache.h"
#include "supercon_data_transfer.h"
typedef struct {
bool ready;
SuperconMeta *meta;
Samples *data_array;
uint16_t data_len;
uint16_t packet_idx;
uint16_t data_idx;
bool is_last_packet;
} TransferCtx;
TransferCtx _cached = {
.ready = false,
.meta = NULL,
.data_array = NULL,
.data_len = 0,
.packet_idx = 0,
.data_idx = 0,
.is_last_packet = false,
};
void stage_data(SuperconMeta *meta, Samples *data_array, uint16_t data_len) {
meta->header.data_len = data_len;
_cached.meta = meta;
_cached.data_array = data_array;
_cached.data_len = data_len;
_cached.is_last_packet = false;
}
bool data_available(void) {
return _cached.ready;
}
static uint8_t get_next_header_byte(void) {
return _cached.meta->bytes[_cached.packet_idx];
}
bool is_last_packet(void) {
if (_cached.packet_idx < I2C_PACKET_SIZE) return false;
/* we're indexing bytes, but data_len is ints */
uint16_t data_bytes_per_packet = I2C_PACKET_SIZE - 4;
uint16_t bytes_remaining = (_cached.data_len * 2) - _cached.data_idx;
return bytes_remaining <= data_bytes_per_packet;
}
static uint8_t get_next_data_byte(void) {
if (_cached.data_idx >= _cached.data_len * 2) {
return 0;
} else {
return _cached.data_array->bytes[_cached.data_idx++];
}
}
uint8_t get_byte_from_int(uint16_t starting_int, uint8_t send_upper_byte) {
if (send_upper_byte) {
return (uint8_t)(starting_int >> 8);
} else {
return (uint8_t)starting_int;
}
}
static uint8_t get_next_in_packet(void) {
uint16_t mod_idx = _cached.packet_idx % I2C_PACKET_SIZE;
uint8_t return_data = 0;
if (mod_idx < 2) {
/* UID */
return_data = get_byte_from_int(_cached.meta->header.uid, mod_idx);
} else if (mod_idx < 4) {
/* Block number */
if (is_last_packet()) {
/* This is the final block, use 0xFFFF for the block number */
return_data = 0xFF;
} else {
return_data = get_byte_from_int(_cached.packet_idx / I2C_PACKET_SIZE, mod_idx % 2);
}
} else if (_cached.packet_idx < I2C_PACKET_SIZE) {
/* Currently sending header */
return_data = get_next_header_byte();
} else {
/* Currently sending data */
return_data = get_next_data_byte();
}
/* Handle increment and checks for end of data */
_cached.packet_idx++;
if (_cached.data_idx >= _cached.data_len * 2) {
/* Set last packet when we run out of data */
_cached.is_last_packet = true;
}
if (_cached.is_last_packet && _cached.packet_idx % I2C_PACKET_SIZE == 0) {
/* This was the last byte of the last packet */
_cached.ready = false;
}
return return_data;
}
uint8_t get_data(void) {
if (!_cached.ready) return -1;
if (_cached.meta == NULL) return -2;
if (_cached.data_array == NULL) return -3;
if (_cached.data_len == 0) return -4;
return get_next_in_packet();
}
bool sample_cache_is_ready(void) {
return _cached.ready;
}
void sample_cache_set_ready(bool ready) {
_cached.ready = ready;
if (ready) {
_cached.packet_idx = 0;
_cached.data_idx = 0;
}
}
#ifndef SUPERCON_DATA_TRANSFER_H
#define SUPERCON_DATA_TRANSFER_H
#include <stdint.h>
#include <stdbool.h>
#include "supercon_data_cache.h"
#define I2C_PACKET_SIZE 36
/* Sizing 36 bytes i2c packets:
*
* Header packet
* 2 unique id
* 2 blocknum (0x00, 0x00 indicates header packet)
* 2 bytes interval between data points (in us)
* 2 bytes total number of sample bytes (# of samples * 2 bytes)
* 25 bytes for name (including null terminaor)
* 3 bytes reserved for future use
*
* Data packets
* 2 unique id
* 2 blocknum (max == final block)
* 32 bytes of data (16 stucts of 8-bit x and 8-bit y pairs)
*/
typedef struct {
uint16_t uid;
uint16_t block_id;
uint16_t interval_us;
uint16_t data_len;
char name[25];
uint8_t u8_reserved;
uint16_t u16_reserved3;
/* total 36 bytes */
} SuperconHeader;
union SuperconMeta_U {
SuperconHeader header;
uint8_t bytes[sizeof(SuperconHeader)];
} typedef SuperconMeta;
struct supercon_packet {
uint16_t uid;
uint16_t blocknum;
Point data[16];
};
void stage_data(SuperconMeta *meta, Samples *data_array, uint16_t data_len);
bool sample_cache_is_ready(void);
void sample_cache_set_ready(bool ready);
bool data_available(void);
uint8_t get_data(void);
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment