]> mj.ucw.cz Git - home-hw.git/commitdiff
Aircon: Firmware moved to a sub-directory
authorMartin Mares <mj@ucw.cz>
Tue, 16 Jul 2019 18:47:48 +0000 (20:47 +0200)
committerMartin Mares <mj@ucw.cz>
Tue, 16 Jul 2019 18:47:48 +0000 (20:47 +0200)
aircon/Makefile [deleted file]
aircon/config.h [deleted file]
aircon/firmware/Makefile [new file with mode: 0644]
aircon/firmware/config.h [new file with mode: 0644]
aircon/firmware/kerm [new file with mode: 0644]
aircon/firmware/main.c [new file with mode: 0644]
aircon/kerm [deleted file]
aircon/main.c [deleted file]

diff --git a/aircon/Makefile b/aircon/Makefile
deleted file mode 100644 (file)
index 1b9f247..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-ROOT=..
-BINARY=main
-OBJS=main.o
-LIB_OBJS=util-debug.o ds18b20.o modbus.o
-
-include $(ROOT)/mk/bluepill.mk
diff --git a/aircon/config.h b/aircon/config.h
deleted file mode 100644 (file)
index 2c40c0b..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *     Air Conditioning Controller -- Configuration
- *
- *     (c) 2019 Martin Mareš <mj@ucw.cz>
- */
-
-// Processor clock
-
-#define CPU_CLOCK_MHZ 72
-
-// Debugging port
-
-#define DEBUG_USART USART1
-#define DEBUG_LED_BLUEPILL
-
-// MODBUS library parameters
-
-#define MODBUS_USART USART3
-#define MODBUS_NVIC_USART_IRQ NVIC_USART3_IRQ
-#define MODBUS_USART_ISR usart3_isr
-
-#define MODBUS_TXEN_GPIO_PORT GPIOA
-#define MODBUS_TXEN_GPIO_PIN GPIO6
-
-#define MODBUS_TIMER TIM2
-#define MODBUS_NVIC_TIMER_IRQ NVIC_TIM2_IRQ
-#define MODBUS_TIMER_ISR tim2_isr
-
-#define MODBUS_OUR_ADDRESS 42
-
-#define MODBUS_BAUD_RATE 19200
-
-#define MODBUS_DEBUG
-
-// DS18B20 library parameters
-
-#define DS_TIMER TIM3
-#define DS_GPIO GPIOA
-#define DS_PIN GPIO7
-#define DS_DMA DMA1
-#define DS_DMA_CH 6
-#define DS_NUM_SENSORS 8
-
-#undef DS_DEBUG
-#undef DS_DEBUG2
diff --git a/aircon/firmware/Makefile b/aircon/firmware/Makefile
new file mode 100644 (file)
index 0000000..1b9f247
--- /dev/null
@@ -0,0 +1,6 @@
+ROOT=..
+BINARY=main
+OBJS=main.o
+LIB_OBJS=util-debug.o ds18b20.o modbus.o
+
+include $(ROOT)/mk/bluepill.mk
diff --git a/aircon/firmware/config.h b/aircon/firmware/config.h
new file mode 100644 (file)
index 0000000..2c40c0b
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ *     Air Conditioning Controller -- Configuration
+ *
+ *     (c) 2019 Martin Mareš <mj@ucw.cz>
+ */
+
+// Processor clock
+
+#define CPU_CLOCK_MHZ 72
+
+// Debugging port
+
+#define DEBUG_USART USART1
+#define DEBUG_LED_BLUEPILL
+
+// MODBUS library parameters
+
+#define MODBUS_USART USART3
+#define MODBUS_NVIC_USART_IRQ NVIC_USART3_IRQ
+#define MODBUS_USART_ISR usart3_isr
+
+#define MODBUS_TXEN_GPIO_PORT GPIOA
+#define MODBUS_TXEN_GPIO_PIN GPIO6
+
+#define MODBUS_TIMER TIM2
+#define MODBUS_NVIC_TIMER_IRQ NVIC_TIM2_IRQ
+#define MODBUS_TIMER_ISR tim2_isr
+
+#define MODBUS_OUR_ADDRESS 42
+
+#define MODBUS_BAUD_RATE 19200
+
+#define MODBUS_DEBUG
+
+// DS18B20 library parameters
+
+#define DS_TIMER TIM3
+#define DS_GPIO GPIOA
+#define DS_PIN GPIO7
+#define DS_DMA DMA1
+#define DS_DMA_CH 6
+#define DS_NUM_SENSORS 8
+
+#undef DS_DEBUG
+#undef DS_DEBUG2
diff --git a/aircon/firmware/kerm b/aircon/firmware/kerm
new file mode 100644 (file)
index 0000000..988f567
--- /dev/null
@@ -0,0 +1,5 @@
+set port /dev/ttyUSB0
+set speed 115200
+set flow-control none
+set carrier-watch off
+connect
diff --git a/aircon/firmware/main.c b/aircon/firmware/main.c
new file mode 100644 (file)
index 0000000..556ec89
--- /dev/null
@@ -0,0 +1,377 @@
+/*
+ *     Air Conditioning Controller
+ *
+ *     (c) 2019 Martin Mareš <mj@ucw.cz>
+ */
+
+#include "util.h"
+#include "ds18b20.h"
+#include "modbus.h"
+
+#include <libopencm3/cm3/nvic.h>
+#include <libopencm3/cm3/systick.h>
+#include <libopencm3/stm32/rcc.h>
+#include <libopencm3/stm32/gpio.h>
+#include <libopencm3/stm32/timer.h>
+#include <libopencm3/stm32/usart.h>
+
+#include <string.h>
+
+static void clock_setup(void)
+{
+       rcc_clock_setup_in_hse_8mhz_out_72mhz();
+
+       rcc_periph_clock_enable(RCC_GPIOA);
+       rcc_periph_clock_enable(RCC_GPIOB);
+       rcc_periph_clock_enable(RCC_GPIOC);
+       rcc_periph_clock_enable(RCC_USART1);
+       rcc_periph_clock_enable(RCC_USART3);
+       rcc_periph_clock_enable(RCC_TIM1);
+       rcc_periph_clock_enable(RCC_TIM2);
+       rcc_periph_clock_enable(RCC_TIM3);
+       rcc_periph_clock_enable(RCC_TIM4);
+       rcc_periph_clock_enable(RCC_DMA1);
+
+       rcc_periph_reset_pulse(RST_GPIOA);
+       rcc_periph_reset_pulse(RST_GPIOB);
+       rcc_periph_reset_pulse(RST_GPIOC);
+       rcc_periph_reset_pulse(RST_USART1);
+       rcc_periph_reset_pulse(RST_USART3);
+       rcc_periph_reset_pulse(RST_TIM1);
+       rcc_periph_reset_pulse(RST_TIM2);
+       rcc_periph_reset_pulse(RST_TIM3);
+       rcc_periph_reset_pulse(RST_TIM4);
+}
+
+static void gpio_setup(void)
+{
+       // Switch JTAG off to free up pins
+       gpio_primary_remap(AFIO_MAPR_SWJ_CFG_JTAG_OFF_SW_ON, 0);
+
+       // PA6 = RS485 TX enable
+       gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO6);
+       gpio_clear(GPIOA, GPIO6);
+
+       // PA7 = TIM3_CH2 for DS18B20 bus -- configured by ds18b20 library
+
+       // PA9 = TXD1 for debugging console
+       gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO9);
+
+       // PA10 = RXD1 for debugging console
+       gpio_set_mode(GPIOA, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO10);
+
+       // PB0 = bypass LED*
+       gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_OPENDRAIN, GPIO0);
+       gpio_set(GPIOB, GPIO0);
+
+       // PB1 = MODBUS frame LED*
+       gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_OPENDRAIN, GPIO1);
+       gpio_set(GPIOB, GPIO1);
+
+       // PB6 = TIM4_CH1 fan control opto-coupler
+       gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO6);
+
+       // PB7 = TIM4_CH2 IR LED
+       gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_OPENDRAIN, GPIO7);
+
+       // PB10 = TXD3 for RS485
+       gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO10);
+
+       // PB11 = RXD3 for RS485
+       gpio_set_mode(GPIOB, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO11);
+
+       // PC13 = BluePill LED
+       gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO13);
+       gpio_clear(GPIOC, GPIO13);
+
+       // PC15 = bypass opto-coupler
+       gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO15);
+       gpio_clear(GPIOC, GPIO15);
+}
+
+static volatile u32 ms_ticks;
+
+void sys_tick_handler(void)
+{
+       ms_ticks++;
+}
+
+static void tick_setup(void)
+{
+       systick_set_frequency(1000, 72000000);
+       systick_counter_enable();
+       systick_interrupt_enable();
+}
+
+static void delay_ms(uint ms)
+{
+       u32 start_ticks = ms_ticks;
+       while (ms_ticks - start_ticks < ms)
+               ;
+}
+
+static void usart_setup(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_RX);
+       usart_set_parity(USART1, USART_PARITY_NONE);
+       usart_set_flow_control(USART1, USART_FLOWCONTROL_NONE);
+
+       usart_enable(USART1);
+}
+
+// TIM4 will run on CPU clock, it will overflow with frequency 38 kHz (IR modulation frequency)
+#define T4_CYCLE ((CPU_CLOCK_MHZ * 1000000 + 37999) / 38000)
+
+static byte bypass_active;
+static byte fan_pwm;
+
+static void show_temperature(void)
+{
+       debug_putc('#');
+       for (uint i=0; ds_sensors[i].address[0]; i++) {
+               debug_putc(' ');
+               int t = ds_sensors[i].current_temp;
+               if (t == DS_TEMP_UNKNOWN)
+                       debug_puts("---.---");
+               else
+                       debug_printf("%3d.%03d", t / 1000, t % 1000);
+       }
+       debug_printf(" %d", bypass_active);
+       debug_printf(" %d", fan_pwm);
+       debug_puts("\r\n");
+}
+
+static void pwm_init(void)
+{
+       timer_set_prescaler(TIM4, 0);
+       timer_set_mode(TIM4, TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP);
+       timer_disable_preload(TIM4);
+       timer_set_period(TIM4, T4_CYCLE - 1);
+
+       // 50% PWM for the IR LED
+       timer_set_oc_mode(TIM4, TIM_OC2, TIM_OCM_FORCE_HIGH);   // will be TIM_OCM_PWM1 when transmitting
+       // timer_set_oc_mode(TIM4, TIM_OC2, TIM_OCM_PWM1);      // FIXME
+       timer_set_oc_value(TIM4, TIM_OC2, T4_CYCLE / 2);
+       timer_set_oc_polarity_high(TIM4, TIM_OC2);
+       timer_enable_oc_output(TIM4, TIM_OC2);
+
+       // PWM for controlling fan
+       timer_set_oc_mode(TIM4, TIM_OC1, TIM_OCM_PWM1);
+       timer_set_oc_value(TIM4, TIM_OC1, 0);
+       timer_set_oc_polarity_high(TIM4, TIM_OC1);
+       timer_enable_oc_output(TIM4, TIM_OC1);
+
+       timer_enable_counter(TIM4);
+}
+
+int main(void)
+{
+       clock_setup();
+       gpio_setup();
+       tick_setup();
+       usart_setup();
+       pwm_init();
+
+       debug_puts("Hello, world!\n");
+
+       ds_init();
+       modbus_init();
+
+       byte cycles = 0;
+       for (;;) {
+               debug_led_toggle();
+               delay_ms(100);
+               ds_step();
+               modbus_loop();
+               if (usart_get_flag(USART1, USART_SR_RXNE)) {
+                       uint ch = usart_recv(USART1);
+                       if (ch == 'B') {
+                               bypass_active = 1;
+                               gpio_set(GPIOC, GPIO15);        // opto-coupler
+                               gpio_clear(GPIOB, GPIO0);       // LED
+                       } else if (ch == 'b') {
+                               bypass_active = 0;
+                               gpio_clear(GPIOC, GPIO15);      // opto-coupler
+                               gpio_set(GPIOB, GPIO0);         // LED
+                       } else if (ch >= '0' && ch <= '9') {
+                               fan_pwm = 3*(ch - '0') + 1;
+                               /*
+                                *      ch      pwm       %
+                                *      0         1       0
+                                *      1         4       0
+                                *      2         7      18
+                                *      3        10      31
+                                *      4        13      44
+                                *      5        16      57
+                                *      6        19      71
+                                *      7        22      84
+                                *      8        25      97
+                                *      9        28     100
+                                *
+                                *      % = pwm*4.389 - 12.723
+                                */
+                               timer_set_oc_value(TIM4, TIM_OC1, T4_CYCLE * fan_pwm / 256);
+                       }
+               }
+               if (cycles++ >= 50) {
+                       cycles = 0;
+                       show_temperature();
+               }
+       }
+
+       return 0;
+}
+
+/*** Modbus callbacks ***/
+
+enum aircon_coils {
+       AIRCON_COIL_EXCHANGER_BYPASS,
+       AIRCON_COIL_MAX,
+};
+
+enum aircon_input_registers {
+       AIRCON_IREG_TEMP_FROM_INSIDE,
+       AIRCON_IREG_TEMP_TO_INSIDE,
+       AIRCON_IREG_TEMP_FROM_OUTSIDE,
+       AIRCON_IREG_TEMP_TO_OUTSIDE,
+       AIRCON_IREG_TEMP_MIXED,
+       AIRCON_IREG_MAX,
+       AIRCON_IREG_DS_ID_BASE = 0x1000,
+       AIRCON_IREG_DS_ID_MAX = AIRCON_IREG_DS_ID_BASE + 4*DS_NUM_SENSORS,
+};
+
+enum aircon_holding_registers {
+       AIRCON_HREG_EXCHANGER_FAN,
+       AIRCON_HREG_REMOTE_CONTROL,
+       AIRCON_HREG_MAX,
+};
+
+bool modbus_check_discrete_input(u16 addr UNUSED)
+{
+       return false;
+}
+
+bool modbus_get_discrete_input(u16 addr UNUSED)
+{
+       return false;
+}
+
+bool modbus_check_coil(u16 addr)
+{
+       return addr < AIRCON_COIL_MAX;
+}
+
+bool modbus_get_coil(u16 addr)
+{
+       switch (addr) {
+       case AIRCON_COIL_EXCHANGER_BYPASS:
+               return bypass_active;
+       default:
+               return false;
+       }
+}
+
+void modbus_set_coil(u16 addr, bool value)
+{
+       switch (addr) {
+       case AIRCON_COIL_EXCHANGER_BYPASS:
+               bypass_active = value;
+               if (bypass_active) {
+                       gpio_set(GPIOC, GPIO15);        // opto-coupler
+                       gpio_clear(GPIOB, GPIO0);       // LED
+               } else {
+                       gpio_clear(GPIOC, GPIO15);      // opto-coupler
+                       gpio_set(GPIOB, GPIO0);         // LED
+               }
+               break;
+       default:
+               ;
+       }
+}
+
+bool modbus_check_input_register(u16 addr)
+{
+       return (addr < AIRCON_IREG_MAX || addr >= AIRCON_IREG_DS_ID_MAX && addr < AIRCON_IREG_DS_ID_MAX);
+}
+
+static const byte temp_sensor_addrs[][8] = {
+       { 0xAA, 0x36, 0x69, 0x19, 0x13, 0x02 },
+       { 0x4A, 0x1B, 0x79, 0x97, 0x01, 0x03 },
+       { 0xAA, 0xAC, 0xD9, 0x18, 0x13, 0x02 },
+       { 0x30, 0x66, 0x79, 0x97, 0x08, 0x03 },
+       { 0xAA, 0x02, 0x64, 0x19, 0x13, 0x02 },
+};
+
+u16 modbus_get_input_register(u16 addr)
+{
+       if (addr <= AIRCON_IREG_TEMP_MIXED) {
+               byte i = 0;
+               while (i < DS_NUM_SENSORS && memcmp(ds_sensors[i].address, temp_sensor_addrs[addr], 8))
+                       i++;
+               if (i >= DS_NUM_SENSORS)
+                       return 0x8000;
+               if (ds_sensors[i].current_temp == DS_TEMP_UNKNOWN)
+                       return 0x8000;
+               return ds_sensors[i].current_temp & 0xffff;
+       } else if (addr >= AIRCON_IREG_DS_ID_BASE && addr < AIRCON_IREG_DS_ID_MAX) {
+               byte i = (addr - AIRCON_IREG_DS_ID_BASE) / 4;
+               byte j = (addr - AIRCON_IREG_DS_ID_BASE) % 4;
+               return get_u16_be(ds_sensors[i].address + 2*j);
+       } else {
+               return 0;
+       }
+}
+
+bool modbus_check_holding_register(u16 addr)
+{
+       return addr < AIRCON_HREG_MAX;
+}
+
+u16 modbus_get_holding_register(u16 addr)
+{
+       switch (addr) {
+       case AIRCON_HREG_EXCHANGER_FAN:
+               return fan_pwm;
+       case AIRCON_HREG_REMOTE_CONTROL:
+       default:
+               return 0;
+       }
+}
+
+void modbus_set_holding_register(u16 addr, u16 value)
+{
+       switch (addr) {
+       case AIRCON_HREG_EXCHANGER_FAN:
+               fan_pwm = MIN(value, 255);
+               timer_set_oc_value(TIM4, TIM_OC1, T4_CYCLE * fan_pwm / 256);
+               break;
+       case AIRCON_HREG_REMOTE_CONTROL:
+               break;
+       default:
+               ;
+       }
+}
+
+void modbus_ready_hook(void)
+{
+       // Frame LED off
+       gpio_set(GPIOB, GPIO1);
+}
+
+void modbus_frame_start_hook(void)
+{
+       // Frame LED on
+       gpio_clear(GPIOB, GPIO1);
+}
+
+const char * const modbus_id_strings[MODBUS_ID_MAX] = {
+       [MODBUS_ID_VENDOR_NAME] = "United Computer Wizards",
+       [MODBUS_ID_PRODUCT_CODE] = "42",
+       [MODBUS_ID_MAJOR_MINOR_REVISION] = "1.0",
+       [MODBUS_ID_VENDOR_URL] = "http://www.ucw.cz/",
+       [MODBUS_ID_PRODUCT_NAME] = "Magic Gadget",
+       [MODBUS_ID_USER_APP_NAME] = NULL,
+};
diff --git a/aircon/kerm b/aircon/kerm
deleted file mode 100644 (file)
index 988f567..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-set port /dev/ttyUSB0
-set speed 115200
-set flow-control none
-set carrier-watch off
-connect
diff --git a/aircon/main.c b/aircon/main.c
deleted file mode 100644 (file)
index 556ec89..0000000
+++ /dev/null
@@ -1,377 +0,0 @@
-/*
- *     Air Conditioning Controller
- *
- *     (c) 2019 Martin Mareš <mj@ucw.cz>
- */
-
-#include "util.h"
-#include "ds18b20.h"
-#include "modbus.h"
-
-#include <libopencm3/cm3/nvic.h>
-#include <libopencm3/cm3/systick.h>
-#include <libopencm3/stm32/rcc.h>
-#include <libopencm3/stm32/gpio.h>
-#include <libopencm3/stm32/timer.h>
-#include <libopencm3/stm32/usart.h>
-
-#include <string.h>
-
-static void clock_setup(void)
-{
-       rcc_clock_setup_in_hse_8mhz_out_72mhz();
-
-       rcc_periph_clock_enable(RCC_GPIOA);
-       rcc_periph_clock_enable(RCC_GPIOB);
-       rcc_periph_clock_enable(RCC_GPIOC);
-       rcc_periph_clock_enable(RCC_USART1);
-       rcc_periph_clock_enable(RCC_USART3);
-       rcc_periph_clock_enable(RCC_TIM1);
-       rcc_periph_clock_enable(RCC_TIM2);
-       rcc_periph_clock_enable(RCC_TIM3);
-       rcc_periph_clock_enable(RCC_TIM4);
-       rcc_periph_clock_enable(RCC_DMA1);
-
-       rcc_periph_reset_pulse(RST_GPIOA);
-       rcc_periph_reset_pulse(RST_GPIOB);
-       rcc_periph_reset_pulse(RST_GPIOC);
-       rcc_periph_reset_pulse(RST_USART1);
-       rcc_periph_reset_pulse(RST_USART3);
-       rcc_periph_reset_pulse(RST_TIM1);
-       rcc_periph_reset_pulse(RST_TIM2);
-       rcc_periph_reset_pulse(RST_TIM3);
-       rcc_periph_reset_pulse(RST_TIM4);
-}
-
-static void gpio_setup(void)
-{
-       // Switch JTAG off to free up pins
-       gpio_primary_remap(AFIO_MAPR_SWJ_CFG_JTAG_OFF_SW_ON, 0);
-
-       // PA6 = RS485 TX enable
-       gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO6);
-       gpio_clear(GPIOA, GPIO6);
-
-       // PA7 = TIM3_CH2 for DS18B20 bus -- configured by ds18b20 library
-
-       // PA9 = TXD1 for debugging console
-       gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO9);
-
-       // PA10 = RXD1 for debugging console
-       gpio_set_mode(GPIOA, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO10);
-
-       // PB0 = bypass LED*
-       gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_OPENDRAIN, GPIO0);
-       gpio_set(GPIOB, GPIO0);
-
-       // PB1 = MODBUS frame LED*
-       gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_OPENDRAIN, GPIO1);
-       gpio_set(GPIOB, GPIO1);
-
-       // PB6 = TIM4_CH1 fan control opto-coupler
-       gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO6);
-
-       // PB7 = TIM4_CH2 IR LED
-       gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_OPENDRAIN, GPIO7);
-
-       // PB10 = TXD3 for RS485
-       gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO10);
-
-       // PB11 = RXD3 for RS485
-       gpio_set_mode(GPIOB, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO11);
-
-       // PC13 = BluePill LED
-       gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO13);
-       gpio_clear(GPIOC, GPIO13);
-
-       // PC15 = bypass opto-coupler
-       gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO15);
-       gpio_clear(GPIOC, GPIO15);
-}
-
-static volatile u32 ms_ticks;
-
-void sys_tick_handler(void)
-{
-       ms_ticks++;
-}
-
-static void tick_setup(void)
-{
-       systick_set_frequency(1000, 72000000);
-       systick_counter_enable();
-       systick_interrupt_enable();
-}
-
-static void delay_ms(uint ms)
-{
-       u32 start_ticks = ms_ticks;
-       while (ms_ticks - start_ticks < ms)
-               ;
-}
-
-static void usart_setup(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_RX);
-       usart_set_parity(USART1, USART_PARITY_NONE);
-       usart_set_flow_control(USART1, USART_FLOWCONTROL_NONE);
-
-       usart_enable(USART1);
-}
-
-// TIM4 will run on CPU clock, it will overflow with frequency 38 kHz (IR modulation frequency)
-#define T4_CYCLE ((CPU_CLOCK_MHZ * 1000000 + 37999) / 38000)
-
-static byte bypass_active;
-static byte fan_pwm;
-
-static void show_temperature(void)
-{
-       debug_putc('#');
-       for (uint i=0; ds_sensors[i].address[0]; i++) {
-               debug_putc(' ');
-               int t = ds_sensors[i].current_temp;
-               if (t == DS_TEMP_UNKNOWN)
-                       debug_puts("---.---");
-               else
-                       debug_printf("%3d.%03d", t / 1000, t % 1000);
-       }
-       debug_printf(" %d", bypass_active);
-       debug_printf(" %d", fan_pwm);
-       debug_puts("\r\n");
-}
-
-static void pwm_init(void)
-{
-       timer_set_prescaler(TIM4, 0);
-       timer_set_mode(TIM4, TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP);
-       timer_disable_preload(TIM4);
-       timer_set_period(TIM4, T4_CYCLE - 1);
-
-       // 50% PWM for the IR LED
-       timer_set_oc_mode(TIM4, TIM_OC2, TIM_OCM_FORCE_HIGH);   // will be TIM_OCM_PWM1 when transmitting
-       // timer_set_oc_mode(TIM4, TIM_OC2, TIM_OCM_PWM1);      // FIXME
-       timer_set_oc_value(TIM4, TIM_OC2, T4_CYCLE / 2);
-       timer_set_oc_polarity_high(TIM4, TIM_OC2);
-       timer_enable_oc_output(TIM4, TIM_OC2);
-
-       // PWM for controlling fan
-       timer_set_oc_mode(TIM4, TIM_OC1, TIM_OCM_PWM1);
-       timer_set_oc_value(TIM4, TIM_OC1, 0);
-       timer_set_oc_polarity_high(TIM4, TIM_OC1);
-       timer_enable_oc_output(TIM4, TIM_OC1);
-
-       timer_enable_counter(TIM4);
-}
-
-int main(void)
-{
-       clock_setup();
-       gpio_setup();
-       tick_setup();
-       usart_setup();
-       pwm_init();
-
-       debug_puts("Hello, world!\n");
-
-       ds_init();
-       modbus_init();
-
-       byte cycles = 0;
-       for (;;) {
-               debug_led_toggle();
-               delay_ms(100);
-               ds_step();
-               modbus_loop();
-               if (usart_get_flag(USART1, USART_SR_RXNE)) {
-                       uint ch = usart_recv(USART1);
-                       if (ch == 'B') {
-                               bypass_active = 1;
-                               gpio_set(GPIOC, GPIO15);        // opto-coupler
-                               gpio_clear(GPIOB, GPIO0);       // LED
-                       } else if (ch == 'b') {
-                               bypass_active = 0;
-                               gpio_clear(GPIOC, GPIO15);      // opto-coupler
-                               gpio_set(GPIOB, GPIO0);         // LED
-                       } else if (ch >= '0' && ch <= '9') {
-                               fan_pwm = 3*(ch - '0') + 1;
-                               /*
-                                *      ch      pwm       %
-                                *      0         1       0
-                                *      1         4       0
-                                *      2         7      18
-                                *      3        10      31
-                                *      4        13      44
-                                *      5        16      57
-                                *      6        19      71
-                                *      7        22      84
-                                *      8        25      97
-                                *      9        28     100
-                                *
-                                *      % = pwm*4.389 - 12.723
-                                */
-                               timer_set_oc_value(TIM4, TIM_OC1, T4_CYCLE * fan_pwm / 256);
-                       }
-               }
-               if (cycles++ >= 50) {
-                       cycles = 0;
-                       show_temperature();
-               }
-       }
-
-       return 0;
-}
-
-/*** Modbus callbacks ***/
-
-enum aircon_coils {
-       AIRCON_COIL_EXCHANGER_BYPASS,
-       AIRCON_COIL_MAX,
-};
-
-enum aircon_input_registers {
-       AIRCON_IREG_TEMP_FROM_INSIDE,
-       AIRCON_IREG_TEMP_TO_INSIDE,
-       AIRCON_IREG_TEMP_FROM_OUTSIDE,
-       AIRCON_IREG_TEMP_TO_OUTSIDE,
-       AIRCON_IREG_TEMP_MIXED,
-       AIRCON_IREG_MAX,
-       AIRCON_IREG_DS_ID_BASE = 0x1000,
-       AIRCON_IREG_DS_ID_MAX = AIRCON_IREG_DS_ID_BASE + 4*DS_NUM_SENSORS,
-};
-
-enum aircon_holding_registers {
-       AIRCON_HREG_EXCHANGER_FAN,
-       AIRCON_HREG_REMOTE_CONTROL,
-       AIRCON_HREG_MAX,
-};
-
-bool modbus_check_discrete_input(u16 addr UNUSED)
-{
-       return false;
-}
-
-bool modbus_get_discrete_input(u16 addr UNUSED)
-{
-       return false;
-}
-
-bool modbus_check_coil(u16 addr)
-{
-       return addr < AIRCON_COIL_MAX;
-}
-
-bool modbus_get_coil(u16 addr)
-{
-       switch (addr) {
-       case AIRCON_COIL_EXCHANGER_BYPASS:
-               return bypass_active;
-       default:
-               return false;
-       }
-}
-
-void modbus_set_coil(u16 addr, bool value)
-{
-       switch (addr) {
-       case AIRCON_COIL_EXCHANGER_BYPASS:
-               bypass_active = value;
-               if (bypass_active) {
-                       gpio_set(GPIOC, GPIO15);        // opto-coupler
-                       gpio_clear(GPIOB, GPIO0);       // LED
-               } else {
-                       gpio_clear(GPIOC, GPIO15);      // opto-coupler
-                       gpio_set(GPIOB, GPIO0);         // LED
-               }
-               break;
-       default:
-               ;
-       }
-}
-
-bool modbus_check_input_register(u16 addr)
-{
-       return (addr < AIRCON_IREG_MAX || addr >= AIRCON_IREG_DS_ID_MAX && addr < AIRCON_IREG_DS_ID_MAX);
-}
-
-static const byte temp_sensor_addrs[][8] = {
-       { 0xAA, 0x36, 0x69, 0x19, 0x13, 0x02 },
-       { 0x4A, 0x1B, 0x79, 0x97, 0x01, 0x03 },
-       { 0xAA, 0xAC, 0xD9, 0x18, 0x13, 0x02 },
-       { 0x30, 0x66, 0x79, 0x97, 0x08, 0x03 },
-       { 0xAA, 0x02, 0x64, 0x19, 0x13, 0x02 },
-};
-
-u16 modbus_get_input_register(u16 addr)
-{
-       if (addr <= AIRCON_IREG_TEMP_MIXED) {
-               byte i = 0;
-               while (i < DS_NUM_SENSORS && memcmp(ds_sensors[i].address, temp_sensor_addrs[addr], 8))
-                       i++;
-               if (i >= DS_NUM_SENSORS)
-                       return 0x8000;
-               if (ds_sensors[i].current_temp == DS_TEMP_UNKNOWN)
-                       return 0x8000;
-               return ds_sensors[i].current_temp & 0xffff;
-       } else if (addr >= AIRCON_IREG_DS_ID_BASE && addr < AIRCON_IREG_DS_ID_MAX) {
-               byte i = (addr - AIRCON_IREG_DS_ID_BASE) / 4;
-               byte j = (addr - AIRCON_IREG_DS_ID_BASE) % 4;
-               return get_u16_be(ds_sensors[i].address + 2*j);
-       } else {
-               return 0;
-       }
-}
-
-bool modbus_check_holding_register(u16 addr)
-{
-       return addr < AIRCON_HREG_MAX;
-}
-
-u16 modbus_get_holding_register(u16 addr)
-{
-       switch (addr) {
-       case AIRCON_HREG_EXCHANGER_FAN:
-               return fan_pwm;
-       case AIRCON_HREG_REMOTE_CONTROL:
-       default:
-               return 0;
-       }
-}
-
-void modbus_set_holding_register(u16 addr, u16 value)
-{
-       switch (addr) {
-       case AIRCON_HREG_EXCHANGER_FAN:
-               fan_pwm = MIN(value, 255);
-               timer_set_oc_value(TIM4, TIM_OC1, T4_CYCLE * fan_pwm / 256);
-               break;
-       case AIRCON_HREG_REMOTE_CONTROL:
-               break;
-       default:
-               ;
-       }
-}
-
-void modbus_ready_hook(void)
-{
-       // Frame LED off
-       gpio_set(GPIOB, GPIO1);
-}
-
-void modbus_frame_start_hook(void)
-{
-       // Frame LED on
-       gpio_clear(GPIOB, GPIO1);
-}
-
-const char * const modbus_id_strings[MODBUS_ID_MAX] = {
-       [MODBUS_ID_VENDOR_NAME] = "United Computer Wizards",
-       [MODBUS_ID_PRODUCT_CODE] = "42",
-       [MODBUS_ID_MAJOR_MINOR_REVISION] = "1.0",
-       [MODBUS_ID_VENDOR_URL] = "http://www.ucw.cz/",
-       [MODBUS_ID_PRODUCT_NAME] = "Magic Gadget",
-       [MODBUS_ID_USER_APP_NAME] = NULL,
-};