/*
- * Workshop Clock
+ * Testing Communication with Sinclair Air Conditioner
*
- * (c) 2020 Martin Mareš <mj@ucw.cz>
+ * (c) 2023 Martin Mareš <mj@ucw.cz>
*/
#include "util.h"
#include <libopencm3/stm32/desig.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/usart.h>
-#include <libopencm3/stm32/i2c.h>
+#include <libopencm3/stm32/spi.h>
+#include <libopencm3/stm32/timer.h>
#include <libopencm3/usb/dfu.h>
#include <libopencm3/usb/usbd.h>
static void clock_init(void)
{
- rcc_clock_setup_in_hse_8mhz_out_72mhz();
+ rcc_clock_setup_pll(&rcc_hse_configs[RCC_CLOCK_HSE8_72MHZ]);
rcc_periph_clock_enable(RCC_GPIOA);
rcc_periph_clock_enable(RCC_GPIOB);
rcc_periph_clock_enable(RCC_GPIOC);
- rcc_periph_clock_enable(RCC_I2C1);
+ rcc_periph_clock_enable(RCC_SPI2);
rcc_periph_clock_enable(RCC_USART1);
rcc_periph_clock_enable(RCC_USB);
+ rcc_periph_clock_enable(RCC_TIM3);
rcc_periph_reset_pulse(RST_GPIOA);
rcc_periph_reset_pulse(RST_GPIOB);
rcc_periph_reset_pulse(RST_GPIOC);
- rcc_periph_reset_pulse(RST_I2C1);
+ rcc_periph_reset_pulse(RST_SPI2);
rcc_periph_reset_pulse(RST_USART1);
rcc_periph_reset_pulse(RST_USB);
+ rcc_periph_reset_pulse(RST_TIM3);
}
static void gpio_init(void)
// PC13 = BluePill LED
gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO13);
gpio_clear(GPIOC, GPIO13);
+
+ // PB13 = SCK2
+ // PB15 = MOSI2
+ gpio_set_mode(GPIOB, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO13);
+ gpio_set_mode(GPIOB, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO15);
}
static void usart_init(void)
;
}
+/*** 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
+ spi_set_receive_only_mode(SPI2);
+ spi_enable_software_slave_management(SPI2);
+ spi_set_nss_low(SPI2);
+ spi_send_lsb_first(SPI2);
+ spi_set_clock_polarity_0(SPI2);
+ spi_set_clock_phase_1(SPI2);
+ spi_enable_rx_buffer_not_empty_interrupt(SPI2);
+ nvic_enable_irq(NVIC_SPI2_IRQ);
+ spi_enable(SPI2);
+
+ // 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);
+ timer_disable_preload(TIM3);
+ timer_one_shot_mode(TIM3);
+ timer_enable_irq(TIM3, TIM_DIER_UIE);
+ nvic_enable_irq(NVIC_TIM3_IRQ);
+}
+
+/*
+ * 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 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)
+{
+ if (SPI_SR(SPI2) & SPI_SR_OVR)
+ tm_overruns++;
+ if (SPI_SR(SPI2) & SPI_SR_RXNE) {
+ byte x = SPI_DR(SPI2) ^ 0xff;
+#if 0
+ if (tm_len < ARRAY_SIZE(tm_buffer))
+ tm_buffer[tm_len++] = x;
+#endif
+ static byte tm_address;
+ if (tm_address) {
+ tm_data[tm_address & 7] = x;
+ tm_address = 0;
+ } else if ((x & 0xc0) == 0xc0) {
+ tm_address = x;
+ }
+ timer_set_period(TIM3, 999);
+ timer_generate_event(TIM3, TIM_EGR_UG);
+ timer_enable_counter(TIM3);
+ }
+}
+
+void tim3_isr(void)
+{
+ if (TIM_SR(TIM3) & TIM_SR_UIF) {
+ TIM_SR(TIM3) &= ~TIM_SR_UIF;
+ tm_timeouts++;
+ spi_set_nss_high(SPI2);
+ spi_set_nss_low(SPI2);
+ }
+}
+
+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_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
+ static byte tm_dumped;
+ if (!tm_dumped && tm_len == ARRAY_SIZE(tm_buffer)) {
+ for (uint i=0; i < tm_len; i++)
+ debug_printf("%02x ", tm_buffer[i]);
+ debug_putc('\n');
+ // tm_dumped = 1;
+ tm_len = 0;
+ }
+#endif
+}
+
/*** USB ***/
static usbd_device *usbd_dev;
debug_printf("Hello, world!\n");
+ tm_init();
usb_init();
u32 last_blink = 0;
if (ms_ticks - last_blink >= 1000) {
debug_led_toggle();
last_blink = ms_ticks;
+ tm_show();
}
if (usb_event_pending) {