commit c5e6e28e6f4e33e2e38866df02d5577795ba7dfb Author: boyska Date: Fri Oct 19 11:58:19 2018 +0200 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..e966553 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +This is a collection of misc libraries, examples, etc. for stm32f103 (aka blue pill) + libopencm3 +(+ FreeRTOS, sometimes) diff --git a/ad9850/ad98xx.c b/ad9850/ad98xx.c new file mode 100644 index 0000000..aa4e9d6 --- /dev/null +++ b/ad9850/ad98xx.c @@ -0,0 +1,48 @@ +// This wants to be a "generic" library for ad9833, ad9850 and ad9851. +// currently, only ad9851 is supported +#include +#include +#include "lib/ad98xx.h" +#include "lib/gpio_utils.h" + + +/* ad98_init will initialize pins, not the whole port. + * That is, you'll need to + * rcc_periph_clock_enable(RCC_${dds.gpioport}) + * */ +void ad98_init(ad98_dds dds) { + gpio_set_mode(dds.gpioport, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, + dds.reset | dds.clock | dds.load | dds.data); + gpio_pulseHigh(dds.gpioport, dds.reset); + gpio_pulseHigh(dds.gpioport, dds.clock); + gpio_pulseHigh(dds.gpioport, dds.load); +} + +static void _send_byte(ad98_dds dds, unsigned char byte) { + for(int i=0; i<8; i++) { // every bit + char bit = ((byte >> i) & 1); + if(bit) { + gpio_set(dds.gpioport, dds.data); + } else { + gpio_clear(dds.gpioport, dds.data); + } + gpio_pulseHigh(dds.gpioport, dds.clock); + } +} + +const float tunings[AD98_MODELS_NUM] = {AD98_AD9833_TUNING, + AD98_AD9850_TUNING, + AD98_AD9851_TUNING}; + +void ad98_set_frequency(ad98_dds dds, double frequency) { // requires dds_init to be called before it + // support AD98_MODEL_AD9850 + uint32_t freqWord; + freqWord = frequency * tunings[dds.model]; + for(char i=0; i<4; i++) { + _send_byte(dds, freqWord & 0xFF); + freqWord >>= 8; + } + _send_byte(dds, 0x01); + gpio_pulseHigh(dds.gpioport, dds.load); +} + diff --git a/ad9850/ad98xx.h b/ad9850/ad98xx.h new file mode 100644 index 0000000..ee84b38 --- /dev/null +++ b/ad9850/ad98xx.h @@ -0,0 +1,33 @@ +// This wants to be a "generic" library for ad9833, ad9850 and ad9851. +// currently, only ad9851 is supported and tested. +// ad9850 is supported but not tested +// ad9833 might just not work at all +#pragma once + +#include + +#define AD98_MODEL_AD9833 (unsigned char) 0 +#define AD98_MODEL_AD9850 (unsigned char) 1 +#define AD98_MODEL_AD9851 (unsigned char) 2 +#define AD98_MODELS_NUM 3 + +#define AD98_AD9833_CLOCK 25000000 +#define AD98_AD9850_CLOCK 125000000 +#define AD98_AD9851_CLOCK 180000000 +#define AD98_AD9833_TUNING 171.8 // (2**32)/AD98_AD9850_CLOCK +#define AD98_AD9850_TUNING 34.36 // (2**32)/AD98_AD9850_CLOCK +#define AD98_AD9851_TUNING 23.86 // (2**32)/AD98_AD9851_CLOCK + +typedef struct { + unsigned char model; // 0 for ad9833, 1 for ad9850 and 2 for ad9851 + // same port for all pins! + uint32_t gpioport; + // pins + uint16_t clock; + uint16_t load; // aka FQ_UD aka FSYNC (on 9833) + uint16_t data; + uint16_t reset; // puoi anche metterlo a massa, in tal caso segnalo come 0 così non viene usato un pin inutile +} ad98_dds; + +void ad98_init(ad98_dds); +void ad98_set_frequency(ad98_dds, double); diff --git a/rotary_tbl/main.c b/rotary_tbl/main.c new file mode 100644 index 0000000..d7d1a74 --- /dev/null +++ b/rotary_tbl/main.c @@ -0,0 +1,205 @@ +#include +#include +#include +#include +#include +#include +#include + +#ifdef __GNUC__ +#define UNUSED(x) UNUSED_##x __attribute__((__unused__)) +#else +#define UNUSED(x) UNUSED_##x +#endif + +//add stack-overflow hook +extern void vApplicationStackOverflowHook(xTaskHandle *pxTask, signed portCHAR *pcTaskName); + +// our tasks +void task_onboardled(void *args); +void task_buttonled(void *args); +static QueueHandle_t btn_q; // coda dall'ISR del pulsante a task_buttonled + +/* + * versione più evoluta del controllo di un rotary encoder + * Questa versione usa comunque un solo canale di interrupt, ma prendendo sia il rising sia il falling. + * L'implementazione, relativamente semplice, della ISR è spiegata in + * http://makeatronics.blogspot.com/2013/02/efficiently-reading-quadrature-with.html + * sostanzialmente invece di fare molte variabili e molti if, come spesso si fa per gestire i rotary encoder, + * viene usata una lookup table, modo compatto e veloce di fare una specie di macchina a stati finiti. + * + * sugli interrupt va segnalato che non sono riuscito a far andare gli interrupt sulle porte GPIOB, o almeno + * sulle B0, B1, B4, B5 + * + * un led (quello built-in) fa on/off con duty cycle variabile + * + * altri due led (porte B8 e C15) per debug, si accendono seguendo il rotary encoder (porte A8 e A9) è premuto + * (mandato a VCC): quando si ruota in un verso, il led rosso è fermo e quello verde cambia; nella direzione + * opposta avviene l'inverso. + * Il rotary controlla inoltre il duty cycle del led on-board. È possibile quindi testare il tutto anche senza + * montare i LED, ma sarà tendenzialmente più difficile debuggare. + * + * OSSERVAZIONI: + * - con questa implementazione, solo un filo più complessa di "rotary", il risultato è molto più stabile + * perché si tolgono transizioni non valide. + * + * + * MONTAGGIO + * - un led va messo tra la porta A8, una resistenza (330ohm) e la massa + * - un led va messo tra la porta B11, una resistenza (330ohm) e la massa + * - il rotary encoder vuole il VCC sul pin centrale, porte B5 e B4 ai lati. + * Per fare "debouncing" si usano dei condensatori da 0.1uF tra ciascun pin "laterale" e il VCC. Ovvero ci + * vuole un condensatore tra B5 e VCC e uno tra B4 e VCC. Il tutto è dunque in parallelo rispetto al rotary. + */ + +static void gpio_setup(void) +{ + + // built-in LED + rcc_periph_clock_enable(RCC_GPIOC); + gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, + GPIO13); + + rcc_periph_clock_enable(RCC_GPIOA); + rcc_periph_clock_enable(RCC_GPIOB); + // ext. rotary button: due pin per la direzione (B4 e B5). Metto l'interrupt solo su uno dei due (B1) + gpio_set_mode(GPIOA, GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO8|GPIO9); + + exti_select_source(EXTI8, GPIOA); + exti_set_trigger(EXTI8, EXTI_TRIGGER_BOTH); + exti_enable_request(EXTI8); + nvic_enable_irq(NVIC_EXTI9_5_IRQ); // occhio! guarda tabella 11-2 a pag. 209 o ti perdi su quale NVIC_EXTI usare + + // ext. led (green) + gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO8); + // ext. led (red) + gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO15); +} + + +int main(void) +{ + rcc_clock_setup_in_hse_8mhz_out_72mhz(); + gpio_setup(); + gpio_set(GPIOB, GPIO8); + gpio_set(GPIOC, GPIO15); + // gpio_clear(GPIOB, GPIO11); + + btn_q = xQueueCreate(10, sizeof(char)); + xTaskCreate(task_buttonled, "BUTTONLED", 50, NULL, configMAX_PRIORITIES - 1, + NULL); + xTaskCreate(task_onboardled, "ONBOARD", 50, NULL, configMAX_PRIORITIES - 1, + NULL); + vTaskStartScheduler(); + for (;;) { + } + + return 0; +} + +int boardLedDuty = 1000; +void task_onboardled(void *UNUSED(args)) +{ + while (1) { + gpio_clear(GPIOC, GPIO13); /* LED on */ + vTaskDelay(pdMS_TO_TICKS(boardLedDuty)); + + gpio_set(GPIOC, GPIO13); /* LED off */ + vTaskDelay(pdMS_TO_TICKS(300)); + } +} + +void task_buttonled(void *UNUSED(args)) +{ + while (1) { + char increasing; + if(xQueueReceive(btn_q, &increasing, pdMS_TO_TICKS(5000)) == pdPASS) + { + if(increasing) { + gpio_toggle(GPIOB, GPIO8); + } else { + gpio_toggle(GPIOC, GPIO15); + } + } + } +} + + +// ISR per controllo dei rotary a singolo interrupt +// la puoi riciclare da un progetto ad un altro, cambiando solamente l'if(change < 0) +// e' importante che questa ISR sia associata all'interrupt del "primo" pin (vedi l'assegnazione di "now") +void exti9_5_isr(void) +{ + static const int8_t lookup_table[] = { + 0,0,0,-1, + 0,0,1,0, + 0,1,0,0, + -1,0,0,0}; + static uint8_t enc_val = 0; + static int8_t enc_count = 0; + + exti_reset_request(EXTI8); + enc_val = enc_val << 2; + // now is in the range [0,3]; lsb is A9, then there is A8 + uint8_t now = ((!!gpio_get(GPIOA, GPIO8)) * 2) + !!gpio_get(GPIOA, GPIO9); + enc_val |= now; + + int8_t change = lookup_table[enc_val & 0b1111]; + enc_count += change; + if(enc_count < 2 && enc_count > -2) { + return; + } + + if(change < 0) { // decrease + if(boardLedDuty < 220) + boardLedDuty = 30; + else + boardLedDuty -= 200; + xQueueSend(btn_q, (void*) 0, 0); + } else { + if(boardLedDuty >= 2300) + boardLedDuty = 2500; + else + boardLedDuty += 200; + xQueueSend(btn_q, (void*) 1, 0); + } + enc_count = 0; +} + +// three "small flashes" (an S in Morse code) on onboard led +// +// questa funzione viene chiamata in caso di stackoverflow su uno dei task; e' +// quindi utile metterci un "segnale" riconoscibile per debug. In questo caso 3 +// flash rapidi. +// +// siccome "tarare" la dimensione dello stack necessario e' difficile, con +// questa funzione si puo' andare "a tentoni" e ridurre la dimensione finche' +// non si ottengono crash +// +// in questa funzione, a quanto pare (ma non ho trovato doc) non si possono +// usare cose come vTaskDelay. Siamo quindi privi di RTOS e per fare gli sleep +// dobbiamo fare i for "all'antica" + +void vApplicationStackOverflowHook(xTaskHandle *UNUSED(pxTask), + signed portCHAR *UNUSED(pcTaskName)) +{ + int i; + for (;;) { + for (char c = 0; c < 3; c++) { + gpio_clear(GPIOC, GPIO13); /* LED on */ + for (i = 0; i < 1000000; + i++) /* small delay */ + __asm__("nop"); + + gpio_set(GPIOC, GPIO13); /* LED off */ + for (i = 0; i < 3000000; i++) { /* a bit more delay */ + __asm__("nop"); + } + } + for (i = 0; i < 10000000; i++) { /* long delay */ + __asm__("nop"); + } + } +} + +// vim: set fdm=marker: diff --git a/rotary_tbl2/main.c b/rotary_tbl2/main.c new file mode 100644 index 0000000..72edee4 --- /dev/null +++ b/rotary_tbl2/main.c @@ -0,0 +1,211 @@ +#include +#include +#include +#include +#include +#include +#include + +#ifdef __GNUC__ +#define UNUSED(x) UNUSED_##x __attribute__((__unused__)) +#else +#define UNUSED(x) UNUSED_##x +#endif + +//add stack-overflow hook +extern void vApplicationStackOverflowHook(xTaskHandle *pxTask, signed portCHAR *pcTaskName); + +// our tasks +void task_onboardled(void *args); +void task_buttonled(void *args); +static QueueHandle_t btn_q; // coda dall'ISR del pulsante a task_buttonled + +/* + * versione più evoluta del controllo di un rotary encoder + * Questa versione usa due canali di interrupt, prendendo sia il rising sia il falling. + * L'implementazione, relativamente semplice, della ISR è spiegata in + * http://makeatronics.blogspot.com/2013/02/efficiently-reading-quadrature-with.html + * sostanzialmente invece di fare molte variabili e molti if, come spesso si fa per gestire i rotary encoder, + * viene usata una lookup table, modo compatto e veloce di fare una specie di macchina a stati finiti. + * + * sugli interrupt va segnalato che non sono riuscito a far andare gli interrupt sulle porte GPIOB, o almeno + * sulle B0, B1, B4, B5 + * + * un led (quello built-in) fa on/off con duty cycle variabile + * + * altri due led (porte B8 e C15) si accendono seguendo il rotary encoder (porte A8 e A9) è premuto (mandato a + * VCC): quando si ruota in un verso, il led rosso è fermo e quello verde cambia; nella direzione opposta + * avviene l'inverso. + * Il rotary controlla inoltre il duty cycle del led on-board. È possibile quindi testare il tutto anche senza + * montare i LED, ma sarà tendenzialmente più difficile debuggare. + * + * OSSERVAZIONI + * - il codice è praticamente uguale a rotary_tbl1 (cambia solo la lookup table), ma vengono usati 2 + * interrupt. Inoltre per come è scritta serve che siano sullo stesso gruppo di interrupt (5-9 oppure 10-15, + * anche di linee diverse in teoria) + * + * MONTAGGIO + * - un led va messo tra la porta A8, una resistenza (330ohm) e la massa + * - un led va messo tra la porta B11, una resistenza (330ohm) e la massa + * - il rotary encoder vuole il VCC sul pin centrale, porte B5 e B4 ai lati. + * Per fare "debouncing" si usano dei condensatori da 0.1uF tra ciascun pin "laterale" e il VCC. Ovvero ci + * vuole un condensatore tra B5 e VCC e uno tra B4 e VCC. Il tutto è dunque in parallelo rispetto al rotary. + */ + +static void gpio_setup(void) +{ + + // built-in LED + rcc_periph_clock_enable(RCC_GPIOC); + gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, + GPIO13); + + rcc_periph_clock_enable(RCC_GPIOA); + rcc_periph_clock_enable(RCC_GPIOB); + // ext. rotary button: due pin per la direzione (B4 e B5). Metto l'interrupt solo su uno dei due (B1) + gpio_set_mode(GPIOA, GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO8|GPIO9); + + exti_select_source(EXTI9, GPIOA); + exti_set_trigger(EXTI9, EXTI_TRIGGER_BOTH); + exti_enable_request(EXTI9); + exti_select_source(EXTI8, GPIOA); + exti_set_trigger(EXTI8, EXTI_TRIGGER_BOTH); + exti_enable_request(EXTI8); + nvic_enable_irq(NVIC_EXTI9_5_IRQ); // occhio! guarda tabella 11-2 a pag. 209 o ti perdi su quale NVIC_EXTI usare + + // ext. led (green): increasing + gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO8); + // ext. led (red): decreasing + gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO15); +} + + +int main(void) +{ + rcc_clock_setup_in_hse_8mhz_out_72mhz(); + gpio_setup(); + gpio_set(GPIOB, GPIO8); + gpio_set(GPIOC, GPIO15); + // gpio_clear(GPIOB, GPIO11); + + btn_q = xQueueCreate(10, sizeof(char)); + xTaskCreate(task_buttonled, "BUTTONLED", 50, NULL, configMAX_PRIORITIES - 1, + NULL); + xTaskCreate(task_onboardled, "ONBOARD", 50, NULL, configMAX_PRIORITIES - 1, + NULL); + vTaskStartScheduler(); + for (;;) { + } + + return 0; +} + +int boardLedDuty = 1000; +void task_onboardled(void *UNUSED(args)) +{ + while (1) { + gpio_clear(GPIOC, GPIO13); /* LED on */ + vTaskDelay(pdMS_TO_TICKS(boardLedDuty)); + + gpio_set(GPIOC, GPIO13); /* LED off */ + vTaskDelay(pdMS_TO_TICKS(300)); + } +} + +void task_buttonled(void *UNUSED(args)) +{ + while (1) { + char increasing; + if(xQueueReceive(btn_q, &increasing, pdMS_TO_TICKS(5000)) == pdPASS) + { + if(increasing) { + gpio_toggle(GPIOB, GPIO8); + } else { + gpio_toggle(GPIOC, GPIO15); + } + } + } +} + + +// ISR per controllo dei rotary a doppio interrupt +// la puoi riciclare da un progetto ad un altro, cambiando solamente l'if(change < 0) +// per avere 2 interrupt su 1 funzione, usiamo il blocco 5-9 che ha proprio questa caratteristica. +// quindi i pin 8 e 9 finiscono entrambi qui +// se dovevamo per forza fare due funzioni diverse, dovevamo +void exti9_5_isr(void) +{ + static const int8_t lookup_table[] = { + 0, -1, 1, 0, + 1, 0, 0, -1, + -1, 0, 0, 1, + 0, 1, -1, 0}; + static uint8_t enc_val = 0; + static int8_t enc_count = 0; + + exti_reset_request(EXTI8|EXTI9); + enc_val = enc_val << 2; + // now is in the range [0,3]; lsb is A9, then there is A8 + uint8_t now = ((!!gpio_get(GPIOA, GPIO8)) * 2) + !!gpio_get(GPIOA, GPIO9); + enc_val |= now; + + int8_t change = lookup_table[enc_val & 0b1111]; + enc_count += change; + + if(enc_count < 4 && enc_count > -4) { + return; + } + + if(enc_count < 0) { // decrease + if(boardLedDuty < 220) + boardLedDuty = 30; + else + boardLedDuty -= 200; + xQueueSend(btn_q, (void*) 0, 0); + } else { + if(boardLedDuty >= 2300) + boardLedDuty = 2500; + else + boardLedDuty += 200; + xQueueSend(btn_q, (void*) 1, 0); + } + enc_count = 0; +} + +// three "small flashes" (an S in Morse code) on onboard led +// +// questa funzione viene chiamata in caso di stackoverflow su uno dei task; e' +// quindi utile metterci un "segnale" riconoscibile per debug. In questo caso 3 +// flash rapidi. +// +// siccome "tarare" la dimensione dello stack necessario e' difficile, con +// questa funzione si puo' andare "a tentoni" e ridurre la dimensione finche' +// non si ottengono crash +// +// in questa funzione, a quanto pare (ma non ho trovato doc) non si possono +// usare cose come vTaskDelay. Siamo quindi privi di RTOS e per fare gli sleep +// dobbiamo fare i for "all'antica" + +void vApplicationStackOverflowHook(xTaskHandle *UNUSED(pxTask), + signed portCHAR *UNUSED(pcTaskName)) +{ + int i; + for (;;) { + for (char c = 0; c < 3; c++) { + gpio_clear(GPIOC, GPIO13); /* LED on */ + for (i = 0; i < 1000000; + i++) /* small delay */ + __asm__("nop"); + + gpio_set(GPIOC, GPIO13); /* LED off */ + for (i = 0; i < 3000000; i++) { /* a bit more delay */ + __asm__("nop"); + } + } + for (i = 0; i < 10000000; i++) { /* long delay */ + __asm__("nop"); + } + } +} + +// vim: set fdm=marker: diff --git a/ssd1306/ssd1306.c b/ssd1306/ssd1306.c new file mode 100644 index 0000000..3e564cf --- /dev/null +++ b/ssd1306/ssd1306.c @@ -0,0 +1,85 @@ +#include + +#include +#include +#include "FreeRTOS.h" +#include "task.h" + +#include "ssd1306.h" + + +static void oled_command(struct ssd1306 *dev, uint8_t byte) { + gpio_clear(dev->port,dev->dc); + spi_enable(dev->spi); + spi_xfer(dev->spi,byte); + spi_disable(dev->spi); +} + +static void oled_command2(struct ssd1306 *dev, uint8_t byte,uint8_t byte2) { + gpio_clear(dev->port,dev->dc); + spi_enable(dev->spi); + spi_xfer(dev->spi,byte); + spi_xfer(dev->spi,byte2); + spi_disable(dev->spi); +} + +static void oled_data(struct ssd1306 *dev, uint8_t byte) { + gpio_set(dev->port,dev->dc); + spi_enable(dev->spi); + spi_xfer(dev->spi,byte); + spi_disable(dev->spi); +} + +void oled_reset(struct ssd1306 *dev) { + gpio_clear(dev->port,dev->rst); + // TODO: that's ugly because it depends on 1) freertos 2) being inside a task + vTaskDelay(1); + gpio_set(dev->port,dev->rst); +} + +void oled_init(struct ssd1306 *dev) { + static uint8_t cmds[] = { + 0xAE, 0x00, 0x10, 0x40, 0x81, 0xCF, 0xA1, 0xA6, + 0xA8, 0x3F, 0xD3, 0x00, 0xD5, 0x80, 0xD9, 0xF1, + 0xDA, 0x12, 0xDB, 0x40, 0x8D, 0x14, 0xAF, 0xFF }; + + oled_reset(dev); + for ( unsigned ux=0; cmds[ux] != 0xFF; ++ux ) + oled_command(dev, cmds[ux]); +} + +void oled_setup(struct ssd1306 *dev) { + gpio_set_mode(dev->port, GPIO_MODE_OUTPUT_2_MHZ, + GPIO_CNF_OUTPUT_PUSHPULL, dev->dc | dev->rst); + gpio_clear(dev->port, dev->rst); + gpio_set_mode(dev->port, GPIO_MODE_OUTPUT_50_MHZ, + GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, + dev->clock | dev->mosi | dev->cs); + spi_reset(dev->spi); + spi_init_master( dev->spi, + SPI_CR1_BAUDRATE_FPCLK_DIV_256, + SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE, + SPI_CR1_CPHA_CLK_TRANSITION_1, + SPI_CR1_DFF_8BIT, + SPI_CR1_MSBFIRST + ); + spi_disable_software_slave_management(dev->spi); + spi_enable_ss_output(dev->spi); +} + +// buf must be exactly 128*64/8 bytes +void oled_draw(struct ssd1306 *dev, uint8_t *buf) { + oled_command2(dev, 0x20,0x02);// Page mode + oled_command(dev, 0x40); + oled_command2(dev, 0xD3,0x00); + for ( uint8_t px=0; px<8; ++px ) { + oled_command(dev, 0xB0|px); + oled_command(dev, 0x00); // Lo col + oled_command(dev, 0x10); // Hi col + for ( unsigned bx=0; bx<128; ++bx ) { + oled_data(dev, *buf++); + } + } +} + + diff --git a/ssd1306/ssd1306.h b/ssd1306/ssd1306.h new file mode 100644 index 0000000..40d8f31 --- /dev/null +++ b/ssd1306/ssd1306.h @@ -0,0 +1,23 @@ +#include + +#ifndef SSD1306_H +#define SSD1306_H +struct ssd1306 { + uint32_t spi; + uint32_t port; + + // those are actually determined by "spi" + uint16_t clock; + uint16_t mosi; + uint16_t cs; // aka NSS + + uint16_t dc; + uint16_t rst; +}; + +void oled_reset(struct ssd1306*); +void oled_init(struct ssd1306*); +void oled_setup(struct ssd1306*); +void oled_draw(struct ssd1306*, uint8_t *buf); + +#endif // SSD1306_H