Browse Source

initial commit

boyska 5 years ago
commit
c5e6e28e6f
7 changed files with 607 additions and 0 deletions
  1. 2 0
      README.md
  2. 48 0
      ad9850/ad98xx.c
  3. 33 0
      ad9850/ad98xx.h
  4. 205 0
      rotary_tbl/main.c
  5. 211 0
      rotary_tbl2/main.c
  6. 85 0
      ssd1306/ssd1306.c
  7. 23 0
      ssd1306/ssd1306.h

+ 2 - 0
README.md

@@ -0,0 +1,2 @@
+This is a collection of misc libraries, examples, etc. for stm32f103 (aka blue pill) + libopencm3
+(+ FreeRTOS, sometimes)

+ 48 - 0
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 <stdint.h>
+#include <libopencm3/stm32/gpio.h>
+#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);
+}
+

+ 33 - 0
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 <stdint.h>
+
+#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);

+ 205 - 0
rotary_tbl/main.c

@@ -0,0 +1,205 @@
+#include <FreeRTOS.h>
+#include <task.h>
+#include <queue.h>
+#include <libopencm3/stm32/gpio.h>
+#include <libopencm3/stm32/rcc.h>
+#include <libopencm3/stm32/exti.h>
+#include <libopencm3/cm3/nvic.h>
+
+#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:

+ 211 - 0
rotary_tbl2/main.c

@@ -0,0 +1,211 @@
+#include <FreeRTOS.h>
+#include <task.h>
+#include <queue.h>
+#include <libopencm3/stm32/gpio.h>
+#include <libopencm3/stm32/rcc.h>
+#include <libopencm3/stm32/exti.h>
+#include <libopencm3/cm3/nvic.h>
+
+#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:

+ 85 - 0
ssd1306/ssd1306.c

@@ -0,0 +1,85 @@
+#include <stdint.h>
+
+#include <libopencm3/stm32/gpio.h>
+#include <libopencm3/stm32/spi.h>
+#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++);
+        }
+	}
+}
+
+

+ 23 - 0
ssd1306/ssd1306.h

@@ -0,0 +1,23 @@
+#include <stdint.h>
+
+#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