]> mj.ucw.cz Git - home-hw.git/blobdiff - test-sinclair/main.c
Auto: Meditation mode turned off
[home-hw.git] / test-sinclair / main.c
index 8de64a79ffc54fa7d509e9050e4b84f0d12eef99..d0194cc28838de19f6eb8b8ef3fcd520fa9a3f2f 100644 (file)
@@ -15,7 +15,6 @@
 #include <libopencm3/stm32/gpio.h>
 #include <libopencm3/stm32/usart.h>
 #include <libopencm3/stm32/spi.h>
-#include <libopencm3/stm32/exti.h>
 #include <libopencm3/stm32/timer.h>
 #include <libopencm3/usb/dfu.h>
 #include <libopencm3/usb/usbd.h>
@@ -34,8 +33,8 @@ static void clock_init(void)
        rcc_periph_clock_enable(RCC_SPI2);
        rcc_periph_clock_enable(RCC_USART1);
        rcc_periph_clock_enable(RCC_USB);
-       rcc_periph_clock_enable(RCC_AFIO);
        rcc_periph_clock_enable(RCC_TIM3);
+       rcc_periph_clock_enable(RCC_TIM4);
 
        rcc_periph_reset_pulse(RST_GPIOA);
        rcc_periph_reset_pulse(RST_GPIOB);
@@ -43,8 +42,8 @@ static void clock_init(void)
        rcc_periph_reset_pulse(RST_SPI2);
        rcc_periph_reset_pulse(RST_USART1);
        rcc_periph_reset_pulse(RST_USB);
-       rcc_periph_reset_pulse(RST_AFIO);
        rcc_periph_reset_pulse(RST_TIM3);
+       rcc_periph_reset_pulse(RST_TIM4);
 }
 
 static void gpio_init(void)
@@ -58,12 +57,15 @@ static void gpio_init(void)
        gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO13);
        gpio_clear(GPIOC, GPIO13);
 
-       // PB12 = SS2 (but used as GP input)
-       // PB13 = SCK2
-       // PB15 = MOSI2
-       gpio_set_mode(GPIOB, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO12);
-       gpio_set_mode(GPIOB, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO13);
-       gpio_set_mode(GPIOB, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO15);
+       // PB13 = SCK2 (pulled up)
+       // PB15 = MOSI2 (pulled up)
+       gpio_set_mode(GPIOB, GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO13);
+       gpio_set_mode(GPIOB, GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO15);
+       gpio_set(GPIOB, GPIO13 | GPIO15);
+
+       // PA8 = IR remote control
+       gpio_clear(GPIOA, GPIO8);
+       gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO8);
 }
 
 static void usart_init(void)
@@ -71,7 +73,7 @@ static void usart_init(void)
        usart_set_baudrate(USART1, 115200);
        usart_set_databits(USART1, 8);
        usart_set_stopbits(USART1, USART_STOPBITS_1);
-       usart_set_mode(USART1, USART_MODE_TX);
+       usart_set_mode(USART1, USART_MODE_TX_RX);
        usart_set_parity(USART1, USART_PARITY_NONE);
        usart_set_flow_control(USART1, USART_FLOWCONTROL_NONE);
 
@@ -103,6 +105,35 @@ static void delay_ms(uint ms)
 
 /*** Emulated TM1618 LED Driver ***/
 
+/*
+ *  Theory of operation:
+ *
+ *  TM1618 communicates using a bi-directional SPI-like protocol.
+ *  The AC unit is sending a stream of commands like this once per ca. 4 ms:
+ *
+ *      00 - set mode: 4 grids, 8 segments
+ *      44 - will write to display memory, no auto-increment
+ *      Cx - set memory address to x
+ *      yy - data to write, two most-significant bits are always zero
+ *      8B - display ON, duty cycle 10/16
+ *
+ *  No read commands are issued, so we can simulate TM1618 using a pure SPI slave.
+ *
+ *  Commands are delimited using the STB* (strobe) pin, but since our opto-couplers
+ *  are negating, we cannot route this pin to SS (slave select) of our SPI.
+ *  We tried triggering an external interrupt by this pin, but it turned out
+ *  that the latency is too high.
+ *
+ *  Instead, we ignore STB* completely and implement a self-synchronizing receiver:
+ *
+ *    - The only byte which can have top 2 bits both set is the Cx command,
+ *      so we can use this to find memory addresses and data in the stream.
+ *      We can ignore all other commands.
+ *
+ *    - Whenever 1 ms passes since the last byte was received, we reset the SPI.
+ *      This allows us to recover from misaligned bytes.
+ */
+
 static void tm_init(void)
 {
        // Configure SPI2 to receive
@@ -116,18 +147,7 @@ static void tm_init(void)
        nvic_enable_irq(NVIC_SPI2_IRQ);
        spi_enable(SPI2);
 
-#if 0
-       // Since our optocouplers are negating, we cannot let STM32 to handle slave
-       // select in hardware. Instead, we let SS trigger an interrupt, which changes
-       // SPI state accordingly.
-       nvic_set_priority(NVIC_EXTI15_10_IRQ, 0);
-       nvic_enable_irq(NVIC_EXTI15_10_IRQ);
-       exti_set_trigger(EXTI12, EXTI_TRIGGER_BOTH);
-       exti_select_source(EXTI12, GPIOB);
-       exti_enable_request(EXTI12);
-#endif
-
-#if 1
+       // TIM3 will handle receive timeout
        timer_set_prescaler(TIM3, CPU_CLOCK_MHZ-1);     // 1 tick = 1 μs
        timer_set_mode(TIM3, TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_DOWN);
        timer_update_on_overflow(TIM3);
@@ -135,50 +155,72 @@ static void tm_init(void)
        timer_one_shot_mode(TIM3);
        timer_enable_irq(TIM3, TIM_DIER_UIE);
        nvic_enable_irq(NVIC_TIM3_IRQ);
-       //timer_set_period(TIM3, 9999);
-       //timer_generate_event(TIM3, TIM_EGR_UG);
-       //timer_enable_counter(TIM3);
-#endif
-}
-
-void exti15_10_isr(void)
-{
-       // We require low latency here, so interaction with peripherals is open-coded.
-#if 0
-       if (GPIO_IDR(GPIOB) & (1 << 12))
-               SPI_CR1(SPI2) |= SPI_CR1_SSI;
-       else
-               SPI_CR1(SPI2) &= ~SPI_CR1_SSI;
-#else
-       SPI_CR1(SPI2) &= ~SPI_CR1_SSI;
-#endif
-       EXTI_PR = EXTI12;
 }
 
+/*
+ *  Data memory of TM1618:
+ *
+ *  [0]    .    .    .    -    -    -    -    -
+ *  [1]    .    .    -    -    HEAT .    .    .
+ *  [2]    .    .    .    DRY  -    SLP  MED  LOW
+ *  [3]    .    .    HIGH AUTO COOL .    .    .
+ *  [4]    .    .    .    B2   -    G2   D2   C2
+ *  [5]    .    .    E2   F2   A2   .    .    .
+ *  [6]    .    .    .    B1   -    G1   D1   C1
+ *  [7]    .    .    E1   F1   A1   .    .    .
+ *
+ *  "." is an always-zero bit not defined by TM1618, "-" is defined, but not used by AC.
+ */
 static volatile byte tm_data[8];
-static volatile byte tm_overrun;
+static volatile uint tm_overruns;
 
 static volatile byte tm_buffer[256];
 static volatile uint tm_len;
 
+/*
+ *
+ *  Display segments:
+ *
+ *      +--A--+
+ *      |     |
+ *      F     B
+ *      |     |
+ *      +--G--+
+ *      |     |
+ *      E     C
+ *      |     |
+ *      +--D--+
+ */
+
+enum tm_seg {
+       SEGa = 0x0800,
+       SEGb = 0x0010,
+       SEGc = 0x0001,
+       SEGd = 0x0002,
+       SEGe = 0x2000,
+       SEGf = 0x1000,
+       SEGg = 0x0004,
+};
+
+static const u16 tm_digits[10] = {
+       [0] = SEGa | SEGb | SEGc | SEGd | SEGe | SEGf,
+       [1] = SEGb | SEGc,
+       [2] = SEGa | SEGb | SEGd | SEGe | SEGg,
+       [3] = SEGa | SEGb | SEGc | SEGd | SEGg,
+       [4] = SEGb | SEGc | SEGf | SEGg,
+       [5] = SEGa | SEGc | SEGd | SEGf | SEGg,
+       [6] = SEGa | SEGc | SEGd | SEGe | SEGf | SEGg,
+       [7] = SEGa | SEGb | SEGc,
+       [8] = SEGa | SEGb | SEGc | SEGd | SEGe | SEGf | SEGg,
+       [9] = SEGa | SEGb | SEGc | SEGd | SEGf | SEGg,
+};
+
 static volatile uint tm_timeouts;
 
 void spi2_isr(void)
 {
-       /*
-        *  The AC unit is sending a stream of commands like this:
-        *
-        *  00 - set mode: 4 grids, 8 segments
-        *  44 - will write to display memory, no auto-increment
-        *  Cx - set memory address to x
-        *  yy - data to write, two most-significant bits are always zero
-        *  8B - display ON, duty cycle 10/16
-        *
-        *  So the only byte which can have top 2 bits both set is the Cx command.
-        *  We make use of this to synchronize the stream.
-        */
        if (SPI_SR(SPI2) & SPI_SR_OVR)
-               tm_overrun = 1;
+               tm_overruns++;
        if (SPI_SR(SPI2) & SPI_SR_RXNE) {
                byte x = SPI_DR(SPI2) ^ 0xff;
 #if 0
@@ -208,35 +250,43 @@ void tim3_isr(void)
        }
 }
 
-#if 0
-static void tm_test(void)
-{
-       u32 start_ticks = ms_ticks;
-       static byte tmbuf[256];
-       uint len = 0;
-
-       while (ms_ticks - start_ticks < 1000 && len < ARRAY_SIZE(tmbuf)) {
-               if (SPI_SR(SPI2) & SPI_SR_RXNE) {
-                       tmbuf[len++] = SPI_DR(SPI2) ^ 0xff;
-               }
-       }
-
-       for (uint i=0; i<len; i++) {
-               debug_printf("%02x ", tmbuf[i]);
-               if ((i % 5) == 4)
-                       debug_putc('\n');
-       }
-       debug_putc('\n');
-}
-#endif
-
 static void tm_show(void)
 {
        debug_printf("TM:");
        for (uint i=0; i<8; i++)
                debug_printf(" %02x", tm_data[i]);
-       debug_printf(" o=%d t=%d", tm_overrun, tm_timeouts);
-       tm_overrun = 0;
+       debug_printf(" o=%d t=%d", tm_overruns, tm_timeouts);
+
+       debug_printf(" =>");
+       if (tm_data[1] & 0x08)
+               debug_printf(" HEAT");
+       if (tm_data[2] & 0x10)
+               debug_printf(" DRY");
+       if (tm_data[2] & 0x04)
+               debug_printf(" SLEEP");
+       if (tm_data[2] & 0x02)
+               debug_printf(" MED");
+       if (tm_data[2] & 0x01)
+               debug_printf(" LOW");
+       if (tm_data[3] & 0x20)
+               debug_printf(" HIGH");
+       if (tm_data[3] & 0x10)
+               debug_printf(" AUTO");
+       if (tm_data[3] & 0x08)
+               debug_printf(" COOL");
+
+       debug_putc(' ');
+       for (int i=0; i<2; i++) {
+               uint x = (tm_data[7-2*i] << 8) | tm_data[6-2*i];
+               uint j = 0;
+               while (j < 10 && tm_digits[j] != x)
+                       j++;
+               if (j == 10)
+                       debug_putc('?');
+               else
+                       debug_putc('0' + j);
+       }
+
        debug_putc('\n');
 
 #if 0
@@ -251,6 +301,247 @@ static void tm_show(void)
 #endif
 }
 
+/*** Infra-red remote control simulator ***/
+
+/*
+ *  The AC unit expects demodulated IR signal. The RC sends 52-bit messages
+ *  (plus leader and trailer). The last 4 bits are a complement of checksum
+ *  of 4-bit nibbles.
+ *
+ *  We represent the messages as two 32-bit words, the upper word containing
+ */
+
+#define RC_POWER_OFF_HI                0b00000000000000000000
+#define RC_POWER_OFF_LO                0b00000000000000010000000010100100
+
+#define RC_DEFAULT_HI          0b00000011000000000000
+
+// Cooling with different fan settings. Combines with a temperature setting (17-30).
+#define RC_COOL_AUTO           0b00000000000000010000000000000000
+#define RC_COOL_HIGH           0b00000000000000010000100000000000
+#define RC_COOL_MED            0b00000000000000010001000100000000
+#define RC_COOL_LOW            0b00000000000000010010001000000000
+
+static const u32 rc_cool_fan[4] = {
+       RC_COOL_AUTO,
+       RC_COOL_LOW,
+       RC_COOL_MED,
+       RC_COOL_HIGH,
+};
+
+// Heating with fixed fan setting. Combines with a temperature setting (15-25).
+#define RC_WARM                        0b00000000000000010000001100000000
+
+// Dehumidifying with fixed fan setting. This is always sent with temperature=17.
+#define RC_DEHUMIDIFY          0b00000000000000010010010000000000
+
+// This can be added to any command to enable sleep mode, but we do not issue it yet.
+#define RC_SLEEP               0b00000000000010000000000000000000
+
+enum rc_mode {
+       MODE_OFF,
+       MODE_COOL,
+       MODE_WARM,
+       MODE_DEHUMIDIFY,
+};
+
+static byte rc_mode = MODE_COOL;       // MODE_xxx
+static byte rc_fan;                    // 0-3
+static byte rc_temp = 17;              // 15-30
+
+static void rc_init(void)
+{
+       // TIM4 runs at 1 MHz and it is used for timing of RC pulses
+       timer_set_prescaler(TIM4, CPU_CLOCK_MHZ - 1);
+       timer_set_mode(TIM4, TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP);
+       timer_update_on_overflow(TIM4);
+       timer_disable_preload(TIM4);
+       timer_one_shot_mode(TIM4);
+       timer_enable_irq(TIM4, TIM_DIER_UIE);
+       nvic_enable_irq(NVIC_TIM4_IRQ);
+}
+
+static u32 rc_pattern[2];
+static uint rc_tick;
+
+static void rc_encode(void)
+{
+       if (rc_mode == MODE_OFF) {
+               rc_pattern[0] = RC_POWER_OFF_HI;
+               rc_pattern[1] = RC_POWER_OFF_LO;
+               return;
+       }
+
+       rc_pattern[0] = RC_DEFAULT_HI;
+       uint t = rc_temp;
+
+       if (rc_mode == MODE_COOL) {
+               rc_pattern[1] = rc_cool_fan[rc_fan];
+               if (t < 17)
+                       t = 17;
+               if (t > 30)
+                       t = 30;
+       } else if (rc_mode == MODE_WARM) {
+               rc_pattern[1] = RC_WARM;
+               if (t < 15)
+                       t = 15;
+               if (t > 25)
+                       t = 25;
+       } else {
+               rc_pattern[1] = RC_DEHUMIDIFY;
+               t = 17;
+       }
+
+       // Encode temperature
+       rc_pattern[1] |= (t - 15) << 4;
+
+       // Compute checksum
+       uint sum = 0;
+       for (uint i=0; i<2; i++)
+               for (uint j=0; j<32; j+=4)
+                       sum += (rc_pattern[i] >> j) & 0x0f;
+       rc_pattern[1] |= (sum & 0x0f) ^ 0x0f;
+}
+
+void tim4_isr(void)
+{
+       if (TIM_SR(TIM4) & TIM_SR_UIF) {
+               TIM_SR(TIM4) &= ~TIM_SR_UIF;
+
+               bool val;       // 1=pulse, 0=break
+               uint duration;  // in μs
+
+               switch (rc_tick) {
+                       case 0:
+                               // Better be safe
+                               return;
+                       case 2:
+                       case 108:
+                               // Initial / final marker
+                               val = 0;
+                               duration = 3600;
+                               // debug_putc('#');
+                               break;
+                       case 110:
+                               // Inter-packet gap
+                               val = 0;
+                               duration = 10000;
+                               // debug_putc('$');
+                               break;
+                       case 111:
+                               // End of message
+                               rc_tick = 0;
+                               return;
+                       default:
+                               if (rc_tick % 2) {
+                                       val = 1;
+                                       duration = 565;
+                                       // debug_putc('*');
+                               } else {
+                                       // Even ticks 4 to 106 transmit 52 bits of data
+                                       uint i = 12 + (rc_tick - 4) / 2;
+                                       val = 0;
+                                       if (rc_pattern[i>>5] & (0x80000000 >> (i & 31))) {
+                                               duration = 1471;
+                                               // debug_putc('B');
+                                       } else {
+                                               duration = 480;
+                                               // debug_putc('A');
+                                       }
+                               }
+               }
+
+               rc_tick++;
+
+               if (val)
+                       gpio_set(GPIOA, GPIO8);
+               else
+                       gpio_clear(GPIOA, GPIO8);
+
+               timer_set_period(TIM4, duration - 1);
+               timer_generate_event(TIM4, TIM_EGR_UG);
+               timer_enable_counter(TIM4);
+       }
+}
+
+static void rc_send(void)
+{
+       if (rc_tick)
+               return;
+
+       rc_encode();
+       debug_printf("RC sending: %05x %08x (mode=%d, fan=%d, temp=%d)\n",
+               (uint) rc_pattern[0], (uint) rc_pattern[1],
+               rc_mode, rc_fan, rc_temp);
+       rc_tick = 1;
+
+       timer_set_period(TIM4, 1);
+       timer_generate_event(TIM4, TIM_EGR_UG);
+       timer_enable_counter(TIM4);
+}
+
+static bool rc_key(char key)
+{
+       if (key == 'o') {
+               rc_mode = MODE_OFF;
+               rc_send();
+               return true;
+       } else if (key == 'c') {
+               rc_mode = MODE_COOL;
+               rc_send();
+               return true;
+       } else if (key == 'w') {
+               rc_mode = MODE_WARM;
+               rc_send();
+               return true;
+       } else if (key == 'd') {
+               rc_mode = MODE_DEHUMIDIFY;
+               rc_send();
+               return true;
+       } else if (key == 'a') {
+               rc_fan = 0;
+               rc_send();
+               return true;
+       } else if (key == 'l') {
+               rc_fan = 1;
+               rc_send();
+               return true;
+       } else if (key == 'm') {
+               rc_fan = 2;
+               rc_send();
+               return true;
+       } else if (key == 'h') {
+               rc_fan = 3;
+               rc_send();
+               return true;
+       } else if (key >= '7' && key <= '9') {
+               rc_temp = key - '0' + 10;
+               rc_send();
+               return true;
+       } else if (key >= '0' && key <= '6') {
+               rc_temp = key - '0' + 20;
+               rc_send();
+               return true;
+       } else if (key == '&') {
+               rc_temp = 27;
+               rc_send();
+               return true;
+       } else if (key == '*') {
+               rc_temp = 28;
+               rc_send();
+               return true;
+       } else if (key == '(') {
+               rc_temp = 29;
+               rc_send();
+               return true;
+       } else if (key == ')') {
+               rc_temp = 30;
+               rc_send();
+               return true;
+       }
+       return false;
+}
+
 /*** USB ***/
 
 static usbd_device *usbd_dev;
@@ -453,6 +744,7 @@ int main(void)
        debug_printf("Hello, world!\n");
 
        tm_init();
+       rc_init();
        usb_init();
 
        u32 last_blink = 0;
@@ -464,6 +756,21 @@ int main(void)
                        tm_show();
                }
 
+               if (usart_get_flag(USART1, USART_SR_RXNE)) {
+                       uint ch = usart_recv(USART1);
+#if 0
+                       if (ch == '1')
+                               gpio_set(GPIOA, GPIO8);
+                       else if (ch == '0')
+                               gpio_clear(GPIOA, GPIO8);
+#else
+                       if (rc_key(ch))
+                               ;
+#endif
+                       else
+                               debug_putc(ch);
+               }
+
                if (usb_event_pending) {
                        usbd_poll(usbd_dev);
                        usb_event_pending = 0;