X-Git-Url: http://mj.ucw.cz/gitweb/?a=blobdiff_plain;f=test-sinclair%2Fmain.c;h=d0194cc28838de19f6eb8b8ef3fcd520fa9a3f2f;hb=refs%2Fheads%2Fmaster;hp=7e8659b4c23c74a8bef74b2f5b07395feb57a4c9;hpb=5039f103adfd9ef31c9881b45e62e7b6da643f58;p=home-hw.git diff --git a/test-sinclair/main.c b/test-sinclair/main.c index 7e8659b..d0194cc 100644 --- a/test-sinclair/main.c +++ b/test-sinclair/main.c @@ -1,7 +1,7 @@ /* - * Workshop Clock + * Testing Communication with Sinclair Air Conditioner * - * (c) 2020 Martin Mareš + * (c) 2023 Martin Mareš */ #include "util.h" @@ -14,7 +14,8 @@ #include #include #include -#include +#include +#include #include #include @@ -24,21 +25,25 @@ 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_clock_enable(RCC_TIM4); 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); + rcc_periph_reset_pulse(RST_TIM4); } static void gpio_init(void) @@ -51,6 +56,16 @@ 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 (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) @@ -58,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); @@ -88,6 +103,445 @@ 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 + 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 +} + +/*** 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; @@ -289,6 +743,8 @@ int main(void) debug_printf("Hello, world!\n"); + tm_init(); + rc_init(); usb_init(); u32 last_blink = 0; @@ -297,6 +753,22 @@ int main(void) if (ms_ticks - last_blink >= 1000) { debug_led_toggle(); last_blink = ms_ticks; + 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) {