/* xdrv_51_bs814a2.ino - BS814A-2 touch buttons support for Tasmota Copyright (C) 2021 Peter Franck and Theo Arends This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_BS814A2 /*********************************************************************************************\ THE PLAN [tm]: OUTCOME: ============== ======== appear in a dropdown (BS814 CLK, BS814 DAT) select 2 pins (clk, data) DONE poll data pin for low state UNRELIABLE generate high-speed clock (25kHz) DONE poll data bits on clock plateau RISING EDGE update data & report every 50 ms DONE error & retry logic (if reqired) BASIC MQTT message & rules hook DONE test on actual Hardware DONE THE PROTOCOL [tm]: ================== see Holtek-Semicon BS814A-2 datasheet for details ********************* * About this driver * ********************* This driver implements the documented protocol of the Holtek BS814A-2 touch controller. The protocol encodes the bitmap of touched keys in a syncronous serial data stream and adds a 3-bit checksum to it. The Chip is supposed to alert the MCU of new key input by pulling the data line low. This mechanism would have been nice but it didn't function reliablly in my hardware sample. So I decided to poll the touch keys every 50 ms regard- less of a change signal. The rest of the driver logic follows pretty much my previous touch driver FTC532. Usage: ------ This driver does not actually switch anything. It is a pure "rules" driver that solely emits {"BS814":{"KEYS":"XX"}} JSON messages to be used in a rule or by an MQTT broker. "XX" stands for the hexadecimal (big endian) representation of a bitmap of keys currently touched, where e.g. "00" means "no key touched" while "03" means "keys 1 and 2 touched simultaneously". Selecting "BS814 CLK" on one GPIO and "BS814 DAT" on another GPIO will awake the driver. This driver can only be selected once. NOTE: Key hex output is sent in 2 digits although it would fit in one. The reason is a provision for BS818A-2 8-key controllers. \*********************************************************************************************/ #define XDRV_51 51 #define BS814_KEYS_MAX 4 // no. of keys supported // Timing in microseconds #define BS814_BIT 40 // serial bit time #define BS814_PULSE 20 // clock pulse width #define BS814_ERR_WAIT 6000 // minimum time before retry #define BS814_FREQ 25000 // max bitrate struct BS814 { uint8_t keys = 0; // bitmap of active & old keys: [ooookkkk] bool present = false; // driver initialized #ifdef DEBUG_BS814_DRIVER uint16_t e_level = 0; // level disagree error uint16_t e_cksum = 0; // checksum errror uint16_t e_stp = 0; // stop bit error bool valid = false; // did we ever receive valid data? #endif // DEBUG_BS814_DRIVER } Bs814; const char Bs814_json[] PROGMEM = "\"BS814\":{\"KEYS\":\""; extern "C" { void os_delay_us(uint32_t); } void bs814_init(void) { // Initialize if (!PinUsed(GPIO_BS814_CLK) || !PinUsed(GPIO_BS814_DAT)) { return; } pinMode(Pin(GPIO_BS814_DAT), INPUT_PULLUP); pinMode(Pin(GPIO_BS814_CLK), OUTPUT); Bs814.present = true; bs814_read(); } void bs814_read(void) { // Poll touch keys uint8_t frame = 0; uint8_t checksum = 0; uint8_t bitp; bool bitval; // generate clock signal & sample frame for (bitp = 0; bitp < 2 * BS814_KEYS_MAX; ++bitp) { digitalWrite(Pin(GPIO_BS814_CLK), LOW); os_delay_us(BS814_PULSE); digitalWrite(Pin(GPIO_BS814_CLK), HIGH); bitval = digitalRead(Pin(GPIO_BS814_DAT)); #ifdef DEBUG_BS814_DRIVER if (bitval != digitalRead(Pin(GPIO_BS814_DAT))) { // value consistent? ++Bs814.e_level; } #endif // DEBUG_BS814_DRIVER frame |= (bitval << bitp); if (bitp < 2 * BS814_KEYS_MAX - 1) { // stop bit if (bitp < BS814_KEYS_MAX) { checksum += bitval; // checksum key bits } os_delay_us(BS814_PULSE); } } // validate frame if (BS814_KEYS_MAX - checksum != ((frame >> 4) & 0x7)) { // checksum error #ifdef DEBUG_BS814_DRIVER ++Bs814.e_cksum; AddLog(LOG_LEVEL_DEBUG, PSTR("CKS=%02X CFR=%02X"), checksum, (frame >> 4) & 0x7); #endif // DEBUG_BS814_DRIVER return; } if ((frame & 0x80) == 0) { // stop bit error #ifdef DEBUG_BS814_DRIVER ++Bs814.e_stp; #endif // DEBUG_BS814_DRIVER return; } Bs814.keys = (Bs814.keys & 0xF0) | (~frame & 0xF); // frame logic inverted #ifdef DEBUG_BS814_DRIVER Bs814.valid = true; #endif // DEBUG_BS814_DRIVER } void bs814_update(void) { // Usually called every 50 ms bs814_read(); if ((Bs814.keys & 0xF) != (Bs814.keys >> 4)) { #ifdef DEBUG_BS814_DRIVER AddLog(LOG_LEVEL_DEBUG, PSTR("BS814: KEY=%0X OLD=%0X LVL=%u CKS=%u STP=%u OK=%u CLK=%u DAT=%u"), Bs814.keys & 0xF, Bs814.keys >> 4, Bs814.e_level, Bs814.e_cksum, Bs814.e_stp, Bs814.valid, Pin(GPIO_BS814_CLK), Pin(GPIO_BS814_DAT)); #endif // DEBUG_BS814_DRIVER bs814_publish(); Bs814.keys = (Bs814.keys << 4) | (Bs814.keys & 0xF); } } void bs814_show() { ResponseAppend_P(PSTR(",%s%02X\"}"), Bs814_json, Bs814.keys & 0xF); // tele json } void bs814_publish(void) { Response_P(PSTR("{%s%02X\"}}"), Bs814_json, Bs814.keys & 0xF); // mqtt & rules MqttPublishTeleSensor(); } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv51(uint8_t function) { bool result = false; if (FUNC_INIT == function) { // Initialize driver bs814_init(); } else if (Bs814.present) { switch (function) { // timed callback functions case FUNC_EVERY_50_MSECOND: bs814_update(); break; // Generate JSON telemetry string case FUNC_JSON_APPEND: bs814_show(); break; } } // Return bool result return result; } #endif // USE_BS814A2