diff --git a/README b/README index bb750e7..a3f9f43 100644 --- a/README +++ b/README @@ -1,2 +1,3 @@ -firmware template for ST STM8S micro-controller. -includes register definitions using macros and structures. +this is the firmware for the dehumidifier sensor module. + +a STM8S003 micro-controller reads the temperature and relative humidity measurements from an AHT20 humidity sensor using I²C, and sends it to the Aplatec DH 10 dehumidifier using a proprietary unidirectional IR-like pulse-width modulated protocol. diff --git a/main.c b/main.c index 442a1e5..f04f4a2 100644 --- a/main.c +++ b/main.c @@ -1,7 +1,8 @@ -/* firmware template for STM8S microcontroller - * Copyright (C) 2019-2020 King Kévin +/* firmware for the ALPATEC DH-10 0101E dehumidifier humidity sensor board + * Copyright (C) 2020 King Kévin * SPDX-License-Identifier: GPL-3.0-or-later */ +/* firmware to read out temperature and relative humidity from AHT20 sensor and send it to Aplatec DH 10 dehumidifier */ #include #include #include "stm8s.h" @@ -9,11 +10,196 @@ // get length of array #define ARRAY_LENGTH(x) (sizeof(x) / sizeof((x)[0])) -// blocking wait (in 10 us steps, up to UINT32_MAX / 10) -static void wait_10us(uint32_t us10) +// pin to send the humidity value to the humidifier +// WHTM-03 board +//#define DEHUMIDIFIER_PORT GPIO_PD +//#define DEHUMIDIFIER_PIN PD5 +// DH10 board +#define DEHUMIDIFIER_PORT GPIO_PD +#define DEHUMIDIFIER_PIN PD2 + +// the I²C address of the AHT20 +#define SENSOR_ADDR 0x38 +#define AHT20_CMD_RESET 0xBA +#define AHT20_CMD_INIT 0xBE +#define AHT20_CMD_TRIGGER 0xAC + +// flag set when value should be sent +volatile bool periodic_flag = false; + +// blocking wait, in us +static void wait_us(uint16_t time) { - us10 = ((us10 / (1 << CLK->CKDIVR.fields.HSIDIV)) * 1000) / 206; // calibrated for 1 ms - for (volatile uint32_t t = 0; t < us10; t++); // burn energy + TIM1->ARRH.reg = (time >> 8) & 0xff; // set timeout + TIM1->ARRL.reg = (time >> 0) & 0xff; // set timeout + TIM1->CR1.fields.CEN = 1; // enable timer + while (TIM1->CR1.fields.CEN); // wait until time passed +} + +// send bit to dehumidifier +static void send_bit(bool bit) +{ + // pulse low + DEHUMIDIFIER_PORT->ODR.reg &= ~DEHUMIDIFIER_PIN; // set low + wait_us(500 - 5); // time with function call delay + // pulse high + DEHUMIDIFIER_PORT->ODR.reg |= DEHUMIDIFIER_PIN; // set high + if (bit) { + wait_us(1500 - 7); // long high pulse + } else { + wait_us(500 - 5); // short high pulse + } +} + +// send byte to dehumidifier (LSb first) +static void send_byte(uint8_t byte) +{ + for (int8_t i = 7; i >= 0; i--) { + send_bit((byte >> i) & 0x01); + } +} + +// send break condition to dehumidifier (after sending the 4 data bytes) +static void send_break(void) +{ + // pulse low + DEHUMIDIFIER_PORT->ODR.reg &= ~DEHUMIDIFIER_PIN; // set low + wait_us(12250 - 759); // long low pulse + DEHUMIDIFIER_PORT->ODR.reg |= DEHUMIDIFIER_PIN; // set high +} + +static uint8_t i2c_transmit(uint8_t addr, const uint8_t* data, uint8_t len) +{ + if (0 == len || 0 == data) { + return 0; + } + I2C_SR2 = 0; // clear errors + I2C_CR2 |= I2C_CR2_START; // send start condition + while (!(I2C_SR1 & I2C_SR1_SB)) { // wait until start bit is sent + if (I2C_SR2 & (I2C_SR2_ARLO | I2C_SR2_BERR)) { // bus error occurred + return 1; + } + } + if (!(I2C_SR3 & I2C_SR3_MSL)) { // ensure we are in master mode + return 2; + } + I2C_SR2 = 0; // clear errors + I2C_DR = (addr << 1) | 0; // select device in transmit + while (!(I2C_SR1 & I2C_SR1_ADDR)) { // wait until address has been transmitted + if (I2C_SR2 & I2C_SR2_BERR) { // bus error occurred + return 3; + } + } + if (I2C_SR2 & I2C_SR2_AF) { // no ACK received + return 4; + } + if (!(I2C_SR3 & I2C_SR3_TRA)) { // ensure we are in transmit mode + return 5; + } + for (uint8_t i = 0; i < len; i++) { + while (!(I2C_SR1 & I2C_SR1_TXE)) { // wait until transmit register is empty + if (I2C_SR2 & I2C_SR2_BERR) { // bus error occurred + return 6; + } + } + if (I2C_SR2 & I2C_SR2_AF) { // no ACK received + return 7; + } + I2C_DR = data[i]; // send data byte + } + while (!(I2C_SR1 & I2C_SR1_TXE)) { // wait until transmit register is empty + if (I2C_SR2 & I2C_SR2_BERR) { // bus error occurred + return 8; + } + } + while (!(I2C_SR1 & I2C_SR1_BTF)) { // wait until transmission completed + if (I2C_SR2 & I2C_SR2_BERR) { // bus error occurred + return 9; + } + } + I2C_CR2 |= I2C_CR2_STOP; // send stop + while (I2C_SR3 & I2C_SR3_BUSY) { // wait until transmission ended + if (I2C_SR2 & I2C_SR2_BERR) { // bus error occurred + return 10; + } + } + if (I2C_SR3 & I2C_SR3_MSL) { // ensure we are back in slave mode + return 11; + } + return 0; +} + +static uint8_t i2c_receive(uint8_t addr, uint8_t* data, uint8_t len) +{ + if (0 == len || 0 == data) { + return 0; + } + I2C_SR2 = 0; // clear errors + I2C_CR2 |= I2C_CR2_START; // send start condition + while (!(I2C_SR1 & I2C_SR1_SB)) { // wait until start bit is sent + if (I2C_SR2 & (I2C_SR2_ARLO | I2C_SR2_BERR)) { // bus error occurred + return 1; + } + } + if (!(I2C_SR3 & I2C_SR3_MSL)) { // ensure we are in master mode + return 2; + } + I2C_SR2 = 0; // clear errors + I2C_DR = (addr << 1) | 1; // select device in receive + while (!(I2C_SR1 & I2C_SR1_ADDR)) { // wait until address has been transmitted + if (I2C_SR2 & I2C_SR2_BERR) { // bus error occurred + return 3; + } + } + if (I2C_SR2 & I2C_SR2_AF) { // no ACK received + return 4; + } + if (I2C_SR3 & I2C_SR3_TRA) { // ensure we are in receive mode + return 5; + } + data[0] = I2C_DR; // just to clear the flag + if (1 == len) { + I2C_CR2 &= ~I2C_CR2_ACK; // already send NACK + I2C_CR2 |= I2C_CR2_STOP; // already program stop + while (!(I2C_SR1 & I2C_SR1_RXNE)) { // wait to receive data + if (I2C_SR2 & I2C_SR2_BERR) { // bus error occurred + return 6; + } + } + data[0] = I2C_DR; // save receive data + } else if (2 == len) { + I2C_CR2 &= ~I2C_CR2_ACK; // already send NACK + while (!(I2C_SR1 & I2C_SR1_BTF)) { // wait until 2 bytes received + if (I2C_SR2 & I2C_SR2_BERR) { // bus error occurred + return 7; + } + } + I2C_CR2 |= I2C_CR2_STOP; // send stop + } else { + I2C_CR2 |= I2C_CR2_ACK; // send ACK + for (uint8_t i = 0; i < len; i++) { + while (!(I2C_SR1 & I2C_SR1_RXNE)) { // wait to receive data + if (I2C_SR2 & I2C_SR2_BERR) { // bus error occurred + return 8; + } + } + data[i] = I2C_DR; // save receive data + if (i + 1 == len) { + I2C_CR2 &= ~I2C_CR2_ACK; // send NACK + I2C_CR2 |= I2C_CR2_STOP; // send stop + } + } + } + while (I2C_SR3 & I2C_SR3_BUSY) { // wait until transmission ended + if (I2C_SR2 & I2C_SR2_BERR) { // bus error occurred + return 10; + } + } + if (I2C_SR3 & I2C_SR3_MSL) { // ensure we are back in slave mode + return 11; + } + + return 0; } void main(void) @@ -24,12 +210,45 @@ void main(void) CLK->CKDIVR.fields.CPUDIV = CLK_CKDIVR_CPUDIV_DIV0; // don't divide CPU frequency to 16 MHz while (!CLK->ICKR.fields.HSIRDY); // wait for internal oscillator to be ready - // configure auto-wakeup (AWU) to be able to refresh the watchdog - // 128 kHz LSI used by default in option bytes CKAWUSEL - // we skip measuring the LS clock frequency since there is no need to be precise - AWU->TBR.fields.AWUTB = 10; // interval range: 128-256 ms - AWU->APR.fields.APR = 0x3e; // set time to 256 ms - AWU_CSR |= AWU_CSR_AWUEN; // enable AWU (start only when entering wait or active halt mode) + // configure dehumidifier output pin + DEHUMIDIFIER_PORT->DDR.reg |= DEHUMIDIFIER_PIN; // switch pin to output + DEHUMIDIFIER_PORT->CR1.reg &= ~DEHUMIDIFIER_PIN; // switch output to open-drain + //DEHUMIDIFIER_PORT->CR1.reg |= DEHUMIDIFIER_PIN; // switch output to push/pull + DEHUMIDIFIER_PORT->ODR.reg |= DEHUMIDIFIER_PIN; // set idle high + + // configure I²C to communicate with ATH20 humidity sensor + CLK_PCKENR1 |= CLK_PCKENR1_I2C; // enable I²C peripheral (should be on per default) + GPIO_PB->CR1.reg |= (PB4 | PB5); // enable internal pull-up on SCL/SDA (there should also be external pull-up resistors) + GPIO_PB->DDR.reg &= ~(PB4 | PB5); // set SCL/SDA as input before it is used as alternate function by the peripheral + I2C_CR1 |= I2C_CR1_PE; // enable I²C peripheral (must be done before any other register is written) + I2C_CR2 |= I2C_CR2_STOP; // release lines + I2C_CR2 |= I2C_CR2_SWRST; // reset peripheral, in case we got stuck and the dog bit + while (!(GPIO_PB->IDR.reg & PB4)); // wait for SCL line to be released + while (!(GPIO_PB->IDR.reg & PB5)); // wait for SDA line to be released + I2C_CR2 &= ~I2C_CR2_SWRST; // release reset + I2C_CR1 &= ~I2C_CR1_PE; // disable I²C peripheral to configure it + I2C_FREQR = 16; // the peripheral frequency is 16 MHz (must match CPU frequency) + I2C_CCRH = (uint8_t)(8000 >> 8); // set control clock to 100 kHz (16 MHz / 2 * 8000), standard mode + I2C_CCRL = (uint8_t)(8000 >> 0); // set control clock to 100 kHz (16 MHz / 2 * 8000) + I2C_TRISER = 0x09; // set maximum rise time for SCL (100 ns in standard mode) + I2C_CR1 |= I2C_CR1_PE; // re-enable I²C peripheral + + // configure timer 2 for periodic 250 ms value sending + TIM2->CR1.fields.URS = 1; // only interrupt on overflow + TIM2->PSCR.fields.PSC = 6; // best prescaler for most precise timer for up to 0.262 second (1.0/(16E6/(2**6)) * 2**16) + TIM2->ARRH.reg = 62500 >> 8; // set the timeout to 250 ms + TIM2->ARRL.reg = 62500 & 0xff; // set the timeout to 250 ms + TIM2->IER.fields.UIE = 1; // enable update/overflow interrupt + TIM2->CR1.fields.CEN = 1; // enable timer for periodic value output + + // configure timer 1 for pulse sending (500 us) + TIM1->CR1.fields.OPM = 1; // only run once + TIM1->CR1.fields.URS = 1; // only interrupt on overflow + TIM1->PSCRH.reg = ((16 - 1) >> 8); // set prescaler to have us + TIM1->PSCRL.reg = ((16 - 1) >> 0); // set prescaler to have us + TIM1->CNTRH.reg = 0; // reset counter value (should be per default) + TIM1->CNTRL.reg = 0; // reset counter value (should be per default) + wait_us(2); // test it once // configure independent watchdog (very loose, just it case the firmware hangs) IWDG->KR.fields.KEY = IWDG_KR_KEY_REFRESH; // reset watchdog @@ -40,18 +259,74 @@ void main(void) rim(); // re-enable interrupts bool action = false; // if an action has been performed + + + wait_us(40000); // wait 40 ms for device to start up + const uint8_t aht20_reset[] = {AHT20_CMD_RESET}; + uint8_t rc = i2c_transmit(SENSOR_ADDR, aht20_reset, ARRAY_LENGTH(aht20_reset)); // send software reset + if (rc) { + while(true); // let the dog bite + } + wait_us(20000); // wait 20 ms for reset to complete + uint8_t rx_buffer[7]; // to receive the measurements + rc = i2c_receive(SENSOR_ADDR, rx_buffer, 1); // read the status + if (rc) { + while(true); // let the dog bite + } + if (!(rx_buffer[0] & 0x08)) { // ensure it is calibrated + const uint8_t aht20_init[] = {AHT20_CMD_INIT}; + rc = i2c_transmit(SENSOR_ADDR, aht20_init, ARRAY_LENGTH(aht20_init)); // initialise device (not sure what it does) + if (rc) { + while(true); // let the dog bite + } + wait_us(10000); // wait 10 ms for calibration to complete + } + // trigger measurement + const uint8_t aht20_trigger[] = {AHT20_CMD_TRIGGER, 0x33, 0x00}; + rc = i2c_transmit(SENSOR_ADDR, aht20_trigger, ARRAY_LENGTH(aht20_trigger)); + if (rc) { + while(true); // let the dog bite + } + wait_us(80000); // wait 80 ms for measurement to complete + + uint8_t humidity = 99; + uint8_t temp = 0; while (true) { IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog + if (periodic_flag) { // time to get and send measurements + periodic_flag = false; // clear flag + rc = i2c_receive(SENSOR_ADDR, rx_buffer, ARRAY_LENGTH(rx_buffer)); // read measurement + if (rc) { + while(true); // let the dog bite + } + if (!(rx_buffer[0] & 0x80)) { // not busy doing the measurement + humidity = (rx_buffer[1] * (uint16_t)100) / 256; // save new humidity value, only use the 8 MSb out of 20 + temp = (((uint8_t)(rx_buffer[3] << 4) + (uint8_t)(rx_buffer[4] >> 4)) * (uint16_t)200) / 256 - 50; // save new temperature, only use the 8 MSB (out of 20) + // trigger next measurement + rc = i2c_transmit(SENSOR_ADDR, aht20_trigger, ARRAY_LENGTH(aht20_trigger)); + if (rc) { + while(true); // let the dog bite + } + } + send_byte(temp); + send_byte(humidity); + send_byte(temp ^ 0xff); + send_byte(humidity ^ 0xff); + send_break(); + } if (action) { // something has been performed, check if other flags have been set meanwhile action = false; // clear flag } else { // nothing down - wfi(); // go to sleep (wait for any interrupt, including periodic AWU) + wfi(); // go to sleep (wait for periodic ping) } } } -void awu(void) __interrupt(IRQ_AWU) // auto wakeup +void tim2_isr(void) __interrupt(IRQ_TIM2_UO) // timer 2 update interrupt service routine { - volatile uint8_t awuf = AWU_CSR; // clear interrupt flag by reading it (reading is required, and volatile prevents compiler optimization) - // let the main loop kick the dog + if (TIM2->SR1.fields.UIF) { // 250 ms passed, time to send data + TIM2->SR1.fields.UIF = 0; // clear flag + periodic_flag = true; // notify main loop + } } +