From 07f75ad6a5cdf035cd575706f0f8c8caf92ee917 Mon Sep 17 00:00:00 2001 From: SSAnkur Date: Sat, 17 Oct 2026 20:39:01 +0600 Subject: [PATCH] Initial Commit --- LYWSD03MMC-BTHome.bin | Bin 0 -> 42596 bytes README.md | 109 ++++++++++++++ makefile | 89 +++++++++++ src/app_config.h | 97 ++++++++++++ src/battery.c | 68 +++++++++ src/battery.h | 12 ++ src/ble.c | 340 ++++++++++++++++++++++++++++++++++++++++++ src/ble.h | 24 +++ src/i2c.c | 61 ++++++++ src/i2c.h | 8 + src/lcd.c | 209 ++++++++++++++++++++++++++ src/lcd.h | 11 ++ src/main.c | 299 +++++++++++++++++++++++++++++++++++++ src/sensor.c | 94 ++++++++++++ src/sensor.h | 18 +++ 15 files changed, 1439 insertions(+) create mode 100644 LYWSD03MMC-BTHome.bin create mode 100644 README.md create mode 100644 makefile create mode 100644 src/app_config.h create mode 100644 src/battery.c create mode 100644 src/battery.h create mode 100644 src/ble.c create mode 100644 src/ble.h create mode 100644 src/i2c.c create mode 100644 src/i2c.h create mode 100644 src/lcd.c create mode 100644 src/lcd.h create mode 100644 src/main.c create mode 100644 src/sensor.c create mode 100644 src/sensor.h diff --git a/LYWSD03MMC-BTHome.bin b/LYWSD03MMC-BTHome.bin new file mode 100644 index 0000000000000000000000000000000000000000..762a078f1d63c59f7f63d18bbd22745609a5f53e GIT binary patch literal 42596 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 j;peAtZG== 0 && s[j] > v) { /* insertion sort */ + s[j + 1] = s[j]; + j--; + } + s[j + 1] = v; + } + + dfifo_disable_dfifo2(); + adc_power_on_sar_adc(0); + + /* mV = avg/8192 * Vref * 8 = sum4 * Vref / 4096 */ + u32 sum = s[2] + s[3] + s[4] + s[5]; + + return (u16)((sum * ADC_VREF_MV + 2048) >> 12); +} diff --git a/src/battery.h b/src/battery.h new file mode 100644 index 0000000..9c442d1 --- /dev/null +++ b/src/battery.h @@ -0,0 +1,12 @@ +#pragma once +#include "tl_common.h" + +/* Low-battery protection, thresholds as pvvx (battery.h, CR2032 build): + * below BOOT_VBAT_MV at power-on the device sleeps again before touching + * flash or the radio; below END_VBAT_MV while running it shows "Lo" and + * sleeps for LOW_VBAT_SLEEP_S, then retries. */ +#define BOOT_VBAT_MV 2200 +#define END_VBAT_MV 2000 +#define LOW_VBAT_SLEEP_S 180 + +u16 battery_read_mv(void); /* VDD (= CR2032) in mV */ diff --git a/src/ble.c b/src/ble.c new file mode 100644 index 0000000..f69986f --- /dev/null +++ b/src/ble.c @@ -0,0 +1,340 @@ +/* + * BLE peripheral: connectable advertising, GATT server with the Telink OTA + * service, and the connection / OTA power handling of + * pvvx/ATC_MiThermometer (ble.c, app_att.c), MIT-style license: + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software ... to deal in the Software without restriction. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + * + * OTA itself (packet handling, CRC, flash writes, swapping the boot bank + * and rebooting) is done by the Telink BLE library: otaWrite()/otaRead(). + * The new image is written to the bank that is not running (0x00000 or + * 0x20000, 128 KiB each), so a failed transfer leaves the old firmware + * bootable. + */ +#include "tl_common.h" +#include "drivers.h" +#include "stack/ble/ble.h" +#include "stack/ble/service/ble_ll_ota.h" +#include "vendor/common/blt_common.h" +#include "app_config.h" +#include "ble.h" +#include "lcd.h" + +void bls_set_advertise_prepare(void *p); /* in the library, no header */ + +/* Peripheral Preferred Connection Parameters (not in this SDK's headers) */ +typedef struct { + u16 intervalMin; + u16 intervalMax; + u16 latency; + u16 timeout; +} gap_periConnectParams_t; + +/* ----- link-layer buffers (required by the library; sizes as pvvx) ----- */ +RAM u8 blt_rxfifo_b[64 * 8] = { 0 }; +RAM my_fifo_t blt_rxfifo = { 64, 8, 0, 0, blt_rxfifo_b, }; +RAM u8 blt_txfifo_b[40 * 16] = { 0 }; +RAM my_fifo_t blt_txfifo = { 40, 16, 0, 0, blt_txfifo_b, }; + +RAM u8 ble_connected; +RAM u8 ota_active; +RAM u8 ota_window; +RAM static u32 window_start; +#define OTA_WINDOW_TICKS ((u32)OTA_WINDOW_S * CLOCK_16M_SYS_TIMER_CLK_1S) + +static void close_window(void); +RAM static u8 conn_param_accepted; + +RAM static u8 mac_public[6]; +static u8 mac_random_static[6]; + +/* Scan response: complete local name DEV_NAME */ +#define NAME_LEN (sizeof(DEV_NAME) - 1) +RAM static u8 scan_rsp[2 + NAME_LEN]; + +/* ============================ GATT table ============================ */ + +enum { + ATT_H_START = 0, + + GAP_PS_H, /* 0x1800 Generic Access */ + GAP_DEVNAME_CD_H, + GAP_DEVNAME_DP_H, + GAP_APPEAR_CD_H, + GAP_APPEAR_DP_H, + GAP_CONNPAR_CD_H, + GAP_CONNPAR_DP_H, + + GATT_PS_H, /* 0x1801 Generic Attribute */ + GATT_SC_CD_H, + GATT_SC_DP_H, + GATT_SC_CCC_H, + + OTA_PS_H, /* Telink OTA */ + OTA_CD_H, + OTA_DP_H, + OTA_DESC_H, + + ATT_END_H, +}; + +static const u16 primary_service_uuid = GATT_UUID_PRIMARY_SERVICE; +static const u16 character_uuid = GATT_UUID_CHARACTER; +static const u16 ccc_uuid = GATT_UUID_CLIENT_CHAR_CFG; +static const u16 user_desc_uuid = GATT_UUID_CHAR_USER_DESC; + +/* GAP */ +static const u16 gap_service_uuid = SERVICE_UUID_GENERIC_ACCESS; +static const u16 devname_uuid = GATT_UUID_DEVICE_NAME; +static const u16 appearance_uuid = GATT_UUID_APPEARANCE; +static const u16 connpar_uuid = GATT_UUID_PERI_CONN_PARAM; +static const u16 appearance = 768; /* Generic Thermometer */ +static const gap_periConnectParams_t periConnParameters = { + CONN_INTERVAL, CONN_INTERVAL, CONN_LATENCY, CONN_TIMEOUT +}; +static const u8 devname_cd[5] = { + CHAR_PROP_READ, + U16_LO(GAP_DEVNAME_DP_H), U16_HI(GAP_DEVNAME_DP_H), + U16_LO(GATT_UUID_DEVICE_NAME), U16_HI(GATT_UUID_DEVICE_NAME) +}; +static const u8 appear_cd[5] = { + CHAR_PROP_READ, + U16_LO(GAP_APPEAR_DP_H), U16_HI(GAP_APPEAR_DP_H), + U16_LO(GATT_UUID_APPEARANCE), U16_HI(GATT_UUID_APPEARANCE) +}; +static const u8 connpar_cd[5] = { + CHAR_PROP_READ, + U16_LO(GAP_CONNPAR_DP_H), U16_HI(GAP_CONNPAR_DP_H), + U16_LO(GATT_UUID_PERI_CONN_PARAM), U16_HI(GATT_UUID_PERI_CONN_PARAM) +}; + +/* GATT */ +static const u16 gatt_service_uuid = SERVICE_UUID_GENERIC_ATTRIBUTE; +static const u16 service_change_uuid = GATT_UUID_SERVICE_CHANGE; +static u32 service_change_val; +RAM static u16 service_change_ccc; +static const u8 service_change_cd[5] = { + CHAR_PROP_INDICATE, + U16_LO(GATT_SC_DP_H), U16_HI(GATT_SC_DP_H), + U16_LO(GATT_UUID_SERVICE_CHANGE), U16_HI(GATT_UUID_SERVICE_CHANGE) +}; + +/* Telink OTA */ +static const u8 ota_service_uuid[16] = TELINK_OTA_UUID_SERVICE; +static const u8 ota_data_uuid[16] = TELINK_SPP_DATA_OTA; +static u8 ota_data; +static const u8 ota_name[] = { 'O', 'T', 'A' }; +static const u8 ota_cd[19] = { + CHAR_PROP_READ | CHAR_PROP_WRITE_WITHOUT_RSP, + U16_LO(OTA_DP_H), U16_HI(OTA_DP_H), + 0x12, 0x2B, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08, + 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00 +}; + + +/* Make the library's OTA timeout an inactivity timeout (pvvx otaWritePre): + * every OTA packet restarts it. */ +static int ota_write_pre(void *p) +{ + blt_ota_start_tick = clock_time() | 1; + return otaWrite(p); +} + +/* In RAM, as pvvx: the stack walks this table from interrupt context, + * which must not depend on flash reads while OTA is erasing/writing it. */ +RAM static attribute_t attributes[] = { + { ATT_END_H - 1, 0, 0, 0, 0, 0, 0, 0 }, /* total number of attributes */ + + { 7, ATT_PERMISSIONS_READ, 2, 2, (u8 *)&primary_service_uuid, (u8 *)&gap_service_uuid, 0, 0 }, + { 0, ATT_PERMISSIONS_READ, 2, sizeof(devname_cd), (u8 *)&character_uuid, (u8 *)devname_cd, 0, 0 }, + { 0, ATT_PERMISSIONS_READ, 2, NAME_LEN, (u8 *)&devname_uuid, &scan_rsp[2], 0, 0 }, + { 0, ATT_PERMISSIONS_READ, 2, sizeof(appear_cd), (u8 *)&character_uuid, (u8 *)appear_cd, 0, 0 }, + { 0, ATT_PERMISSIONS_READ, 2, sizeof(appearance), (u8 *)&appearance_uuid, (u8 *)&appearance, 0, 0 }, + { 0, ATT_PERMISSIONS_READ, 2, sizeof(connpar_cd), (u8 *)&character_uuid, (u8 *)connpar_cd, 0, 0 }, + { 0, ATT_PERMISSIONS_READ, 2, sizeof(periConnParameters), (u8 *)&connpar_uuid, (u8 *)&periConnParameters, 0, 0 }, + + { 4, ATT_PERMISSIONS_READ, 2, 2, (u8 *)&primary_service_uuid, (u8 *)&gatt_service_uuid, 0, 0 }, + { 0, ATT_PERMISSIONS_READ, 2, sizeof(service_change_cd), (u8 *)&character_uuid, (u8 *)service_change_cd, 0, 0 }, + { 0, ATT_PERMISSIONS_READ, 2, sizeof(service_change_val), (u8 *)&service_change_uuid, (u8 *)&service_change_val, 0, 0 }, + { 0, ATT_PERMISSIONS_RDWR, 2, sizeof(service_change_ccc), (u8 *)&ccc_uuid, (u8 *)&service_change_ccc, 0, 0 }, + + { 4, ATT_PERMISSIONS_READ, 2, 16, (u8 *)&primary_service_uuid, (u8 *)ota_service_uuid, 0, 0 }, + { 0, ATT_PERMISSIONS_READ, 2, sizeof(ota_cd), (u8 *)&character_uuid, (u8 *)ota_cd, 0, 0 }, + { 0, ATT_PERMISSIONS_RDWR, 16, sizeof(ota_data), (u8 *)ota_data_uuid, &ota_data, &ota_write_pre, &otaRead }, + { 0, ATT_PERMISSIONS_READ, 2, sizeof(ota_name), (u8 *)&user_desc_uuid, (u8 *)ota_name, 0, 0 }, + +}; + +/* ======================= connection / OTA events ======================= */ + +/* OTA start command received: full radio duty until it is done */ +static void ota_start_cb(void) +{ + ota_active = 1; + conn_param_accepted = 0; + bls_pm_setManualLatency(0); + bls_ota_setTimeout(OTA_IDLE_TIMEOUT_US); +} + +/* After a successful OTA the library reboots into the new image itself; + * this only matters for failures. */ +static void ota_result_cb(int result) +{ + (void)result; + ota_active = 0; +} + +/* Ask for 20 ms interval + latency 49: the device then only has to wake for + * one connection event per second while idle (pvvx ble_connect_callback). */ +static void connect_cb(u8 e, u8 *p, int n) +{ + (void)e; (void)p; (void)n; + ble_connected = 1; + conn_param_accepted = 0; + bls_l2cap_setMinimalUpdateReqSendingTime_after_connCreate(1000); + bls_l2cap_requestConnParamUpdate(CONN_INTERVAL, CONN_INTERVAL, + CONN_LATENCY, CONN_TIMEOUT); + lcd_show_ble(1); +} + +static void disconnect_cb(u8 e, u8 *p, int n) +{ + (void)e; (void)p; (void)n; + ble_connected = 0; + ota_active = 0; + lcd_show_ble(ota_window); + /* the stack resumes advertising by itself; ble_task() switches it to + * non-connectable if the window has expired meanwhile */ +} + +static int conn_param_rsp_cb(u8 id, u16 result) +{ + (void)id; + if (result == CONN_PARAM_UPDATE_ACCEPT) { + conn_param_accepted = 1; + } else if (result == CONN_PARAM_UPDATE_REJECT) { + bls_l2cap_requestConnParamUpdate(CONN_INTERVAL, CONN_INTERVAL, + CONN_LATENCY, CONN_TIMEOUT); + } + return 0; +} + +/* Suspend loses the RF power setting (pvvx suspend_exit_cb) */ +_attribute_ram_code_ +static void suspend_exit_cb(u8 e, u8 *p, int n) +{ + (void)e; (void)p; (void)n; + rf_set_power_level_index(RF_TX_POWER); +} + +/* ============================== init ============================== */ + +void ble_init(const u8 *adv, int adv_len, int (*adv_prepare)(void *), + int open_window) +{ + /* Factory public address (A4:C1:38:...) from the stock flash layout */ + blc_initMacAddress(CFG_ADR_MAC, mac_public, mac_random_static); + + memcpy(&scan_rsp[2], DEV_NAME, NAME_LEN); + scan_rsp[0] = NAME_LEN + 1; + scan_rsp[1] = GAP_ADTYPE_LOCAL_NAME_COMPLETE; + + /* Controller */ + blc_ll_initStandby_module(mac_public); + blc_ll_initAdvertising_module(mac_public); + blc_ll_initConnection_module(); + blc_ll_initSlaveRole_module(); + + /* Host */ + blc_gap_peripheral_init(); + bls_att_setAttributeTable((u8 *)attributes); + blc_l2cap_register_handler(blc_l2cap_packet_receive); + blc_smp_setSecurityLevel(No_Security); + + rf_set_power_level_index(RF_TX_POWER); + bls_app_registerEventCallback(BLT_EV_FLAG_CONNECT, &connect_cb); + bls_app_registerEventCallback(BLT_EV_FLAG_TERMINATE, &disconnect_cb); + bls_app_registerEventCallback(BLT_EV_FLAG_SUSPEND_EXIT, &suspend_exit_cb); + blc_l2cap_registerConnUpdateRspCb(conn_param_rsp_cb); + + /* OTA: erase the inactive bank if it still holds an old image */ + bls_ota_clearNewFwDataArea(); + bls_ota_registerStartCmdCb(ota_start_cb); + bls_ota_registerResultIndicateCb(ota_result_cb); + + /* Advertising */ + bls_ll_setAdvData((u8 *)adv, adv_len); + bls_ll_setScanRspData(scan_rsp, sizeof(scan_rsp)); + bls_set_advertise_prepare((void *)adv_prepare); + if (open_window) { + /* 1 s interval so the flashing page finds and connects quickly */ + ota_window = 1; + window_start = clock_time(); + bls_ll_setAdvParam(OTA_WINDOW_ADV_INTERVAL, + OTA_WINDOW_ADV_INTERVAL + ADV_INTERVAL_DELAY, + ADV_TYPE_CONNECTABLE_UNDIRECTED, OWN_ADDRESS_PUBLIC, + 0, NULL, BLT_ENABLE_ADV_ALL, ADV_FP_NONE); + bls_ll_setAdvEnable(BLC_ADV_ENABLE); + lcd_show_ble(1); /* BLE symbol = OTA window open */ + } else { + close_window(); + } + + /* Power management (pvvx init_ble) */ + blc_ll_initPowerManagement_module(); + blc_pm_setDeepsleepRetentionThreshold(40, 18); + blc_pm_setDeepsleepRetentionEarlyWakeupTiming(240); + blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW32K); + bls_pm_setSuspendMask(SUSPEND_ADV | DEEPSLEEP_RETENTION_ADV | + SUSPEND_CONN | DEEPSLEEP_RETENTION_CONN); + bls_pm_setManualLatency(0); +} + +/* Called every main-loop pass. During OTA: answer every connection event + * (no slave latency) and never use deep-sleep retention, which would drop + * the transfer state. Otherwise allow the deepest sleep everywhere. */ +/* Window over: non-connectable advertising at the normal interval. In + * this mode the radio does not listen after each packet. */ +static void close_window(void) +{ + ota_window = 0; + bls_ll_setAdvParam(ADV_INTERVAL, ADV_INTERVAL + ADV_INTERVAL_DELAY, + ADV_TYPE_NONCONNECTABLE_UNDIRECTED, OWN_ADDRESS_PUBLIC, + 0, NULL, BLT_ENABLE_ADV_ALL, ADV_FP_NONE); + bls_ll_setAdvEnable(BLC_ADV_ENABLE); + lcd_show_ble(0); +} + +_attribute_ram_code_ +int ble_task(void) +{ + /* An upload that started inside the window may finish after it; any + * other connection is dropped when the window ends. */ + if (ota_window && !ota_active && + clock_time() - window_start > OTA_WINDOW_TICKS) { + if (ble_connected) { + bls_ll_terminateConnection(HCI_ERR_REMOTE_USER_TERM_CONN); + } else { + close_window(); + } + } + if (ota_active) { + if (!conn_param_accepted) { + bls_pm_setManualLatency(0); + } + bls_pm_setSuspendMask(SUSPEND_ADV | SUSPEND_CONN); + return 1; + } + bls_pm_setSuspendMask(SUSPEND_ADV | DEEPSLEEP_RETENTION_ADV | + SUSPEND_CONN | DEEPSLEEP_RETENTION_CONN); + return 0; +} + +_attribute_ram_code_ +void ble_init_deep_retention(void) +{ + rf_set_power_level_index(RF_TX_POWER); + blc_ll_recoverDeepRetention(); + bls_ota_registerStartCmdCb(ota_start_cb); /* as pvvx */ +} diff --git a/src/ble.h b/src/ble.h new file mode 100644 index 0000000..79edf72 --- /dev/null +++ b/src/ble.h @@ -0,0 +1,24 @@ +#pragma once +#include "tl_common.h" + +/* Connectable advertising + GATT server with the Telink OTA service + * (00010203-0405-0607-0809-0a0b0c0d1912), structured like + * pvvx/ATC_MiThermometer ble.c / app_att.c. */ + +extern u8 ble_connected; /* a central is connected */ +extern u8 ota_active; /* OTA transfer in progress */ +extern u8 ota_window; /* connectable OTA window open */ + +/* Called once from user_init_normal(). open_window: battery was just + * inserted, accept connections for OTA_WINDOW_S; otherwise go straight to + * non-connectable advertising. adv_prepare is called by the stack + * right before every advertising event (pvvx app_advertise_prepare_handler): + * the application piggybacks its periodic work on those wake-ups. */ +void ble_init(const u8 *adv, int adv_len, int (*adv_prepare)(void *), + int open_window); + +/* Every main-loop pass: sets the sleep mode; returns 1 while OTA runs */ +int ble_task(void); + +/* Restore what deep-sleep retention loses (from user_init_deepRetn) */ +void ble_init_deep_retention(void); diff --git a/src/i2c.c b/src/i2c.c new file mode 100644 index 0000000..b9f8a08 --- /dev/null +++ b/src/i2c.c @@ -0,0 +1,61 @@ +#include "tl_common.h" +#include "drivers.h" +#include "app_config.h" +#include "i2c.h" + +#define I2C_WAIT() while (reg_i2c_status & FLD_I2C_CMD_BUSY) + +/* The I2C clock and pin mux live in digital registers that are lost in + * deep-sleep retention, so this is called at the start of every bus session. + * Clock stretching is disabled: the sensor is polled, never stretched. */ +_attribute_ram_code_ +void i2c_bus_init(void) +{ + i2c_gpio_set(I2C_GROUP); + reg_i2c_speed = CLOCK_SYS_CLOCK_HZ / (4 * I2C_SPEED_HZ); + reg_i2c_mode |= FLD_I2C_MASTER_EN; + reg_i2c_mode &= ~FLD_I2C_HOLD_MASTER; + reg_clk_en0 |= FLD_CLK0_I2C_EN; + reg_spi_sp &= ~FLD_SPI_ENABLE; /* pads shared with SPI: force I2C */ +} + +_attribute_ram_code_ +int i2c_write(u8 addr, const u8 *buf, u32 len) +{ + int err; + + reg_i2c_id = addr << 1; + reg_i2c_ctrl = FLD_I2C_CMD_START | FLD_I2C_CMD_ID; + I2C_WAIT(); + err = reg_i2c_status & FLD_I2C_NAK; + while (!err && len--) { + reg_i2c_do = *buf++; + reg_i2c_ctrl = FLD_I2C_CMD_DO; + I2C_WAIT(); + err = reg_i2c_status & FLD_I2C_NAK; + } + reg_i2c_ctrl = FLD_I2C_CMD_STOP; + I2C_WAIT(); + return err; +} + +_attribute_ram_code_ +int i2c_read(u8 addr, u8 *buf, u32 len) +{ + int err; + + reg_i2c_id = (addr << 1) | FLD_I2C_WRITE_READ_BIT; + reg_i2c_ctrl = FLD_I2C_CMD_START | FLD_I2C_CMD_ID; + I2C_WAIT(); + err = reg_i2c_status & FLD_I2C_NAK; + while (!err && len--) { + /* NAK (ACK flag) the last byte to end the read */ + reg_i2c_ctrl = FLD_I2C_CMD_DI | FLD_I2C_CMD_READ_ID | + (len ? 0 : FLD_I2C_CMD_ACK); + I2C_WAIT(); + *buf++ = reg_i2c_di; + } + reg_i2c_ctrl = FLD_I2C_CMD_STOP; + I2C_WAIT(); + return err; +} diff --git a/src/i2c.h b/src/i2c.h new file mode 100644 index 0000000..46df685 --- /dev/null +++ b/src/i2c.h @@ -0,0 +1,8 @@ +#pragma once +#include "tl_common.h" + +/* Polled I2C master on the TLSR825x hardware block. Addresses are 7-bit. + * All functions return 0 on success, non-zero on NAK. */ +void i2c_bus_init(void); +int i2c_write(u8 addr, const u8 *buf, u32 len); +int i2c_read(u8 addr, u8 *buf, u32 len); diff --git a/src/lcd.c b/src/lcd.c new file mode 100644 index 0000000..43ac035 --- /dev/null +++ b/src/lcd.c @@ -0,0 +1,209 @@ +/* + * LYWSD03MMC segment LCD driver for the 2025 "B1.5" board (pvvx reports it + * as HW B1.1): LCD controller on bit-banged SPI, PD7 = CLK, PB7 = SDI. + * Write-only, no acknowledge. + * + * The frame format and segment map are from pvvx/ATC_MiThermometer + * (lcd_lywsd03mmc.c), used under that project's MIT-style license: + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software ... to deal in the Software without restriction. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + * + * Display buffer, byte.bit: + * + * --5.4-- --4.4-- --3.4-- BLE 2.4 + * | 5.5 5.0 4.5 4.0 3.5 3.0 o 2.5 (temp unit + * 5.3 --5.1-- --4.1-- --3.1-- 2.5..2.7 symbols) + * | 5.6 5.2 4.6 4.2 3.6 3.2 + * --5.7-- --4.7-- * --3.7-- + * 4.3 + * smiley 2.0..2.2 --1.4-- --0.4-- + * 1.5 1.0 0.5 0.0 + * --1.1-- --0.1-- + * 1.6 1.2 0.6 0.2 % 0.3 + * BAT 1.3 --1.7-- --0.7-- + */ +#include "tl_common.h" +#include "drivers.h" +#include "drivers/8258/gpio_8258.h" +#include "app_config.h" +#include "lcd.h" + +#define SEG_MINUS 0x02 /* middle segment */ +#define SYM_POINT 0x08 /* 4.3 */ +#define SYM_ONE 0x08 /* 5.3: leading "1" for 100..199 */ +#define SYM_PERCENT 0x08 /* 0.3 */ +#define SYM_BAT 0x08 /* 1.3 */ +#define SYM_DEG_C 0xA0 /* 2.5 + 2.7 */ +#define SYM_BLE 0x10 /* 2.4: shown while a BLE central is connected */ +#define SYM_H 0x67 +#define SYM_i 0x40 +#define SYM_L 0xE0 +#define SYM_o 0xC6 + +#define SPI_CLK_DELAY_US 32 /* half clock period: ~15 kHz */ + +static const u8 digits[10] = { + 0xF5, 0x05, 0xD3, 0x97, 0x27, 0xB6, 0xF6, 0x15, 0xF7, 0xB7 +}; + +/* Frame: AA d5..d0 xor(d) 55, sent byte by byte, LSB first */ +RAM static u8 buf[6]; +RAM static u8 sent[6]; +RAM static u8 frame[9]; + +static void frame_fill(void) +{ + u8 chk = 0; + + frame[0] = 0xAA; + for (int i = 0; i < 6; i++) { + frame[1 + i] = buf[5 - i]; + chk ^= buf[i]; + } + frame[7] = chk; + frame[8] = 0x55; +} + +/* Data changes while CLK is low and is latched on the rising edge */ +_attribute_ram_code_ +static void spi_send_byte(u8 b) +{ + for (int i = 0; i < 8; i++) { + BM_CLR(reg_gpio_out(GPIO_LCD_CLK), GPIO_LCD_CLK & 0xFF); + if (b & 0x01) { + BM_SET(reg_gpio_out(GPIO_LCD_SDI), GPIO_LCD_SDI & 0xFF); + } else { + BM_CLR(reg_gpio_out(GPIO_LCD_SDI), GPIO_LCD_SDI & 0xFF); + } + sleep_us(SPI_CLK_DELAY_US); + BM_SET(reg_gpio_out(GPIO_LCD_CLK), GPIO_LCD_CLK & 0xFF); + sleep_us(SPI_CLK_DELAY_US); + b >>= 1; + } + sleep_us(SPI_CLK_DELAY_US); +} + +/* ~5 ms with interrupts off, only when the content changed. gpio_init() + * makes both pins inputs again after every deep-sleep retention wake-up, + * so they are switched to outputs on each send. */ +_attribute_ram_code_ +static void lcd_send(void) +{ + BM_CLR(reg_gpio_oen(GPIO_LCD_CLK), GPIO_LCD_CLK & 0xFF); + BM_CLR(reg_gpio_oen(GPIO_LCD_SDI), GPIO_LCD_SDI & 0xFF); + + u8 r = irq_disable(); + + for (int i = 0; i < (int)sizeof(frame); i++) { + spi_send_byte(frame[i]); + } + irq_restore(r); +} + +static void lcd_flush(void) +{ + if (!memcmp(sent, buf, sizeof(buf))) { + return; /* nothing changed: no LCD traffic at all */ + } + frame_fill(); + lcd_send(); + memcpy(sent, buf, sizeof(buf)); +} + +void lcd_init(void) +{ + memset(buf, SEG_MINUS, sizeof(buf)); /* "-- --" */ + buf[2] = 0; + + pm_wait_ms(30); /* controller start-up, as pvvx (>= 30 ms) */ + frame_fill(); + lcd_send(); + memcpy(sent, buf, sizeof(buf)); +} + +/* Big digits: temperature in 0.1 degC. + * -9.9 .. 199.9 shown with one decimal, -99 .. -10 as integers. */ +static void show_big_x10(s16 n) +{ + buf[5] = buf[4] = 0; + if (n > 1999) { + buf[3] = 0; buf[4] = SYM_i; buf[5] = SYM_H; /* "Hi" */ + return; + } + if (n <= -995) { + buf[3] = 0; buf[4] = SYM_o; buf[5] = SYM_L; /* "Lo" */ + return; + } + if (n < 0) { + n = -n; + buf[5] = SEG_MINUS; + } + if (buf[5] && n > 99) { + n = (n + 5) / 10; /* -10 .. -99: integer */ + } else { + buf[4] = SYM_POINT; + } + if (n > 999) { + buf[5] |= SYM_ONE; + } + if (n > 99) { + buf[5] |= digits[n / 100 % 10]; + } + buf[4] |= digits[n / 10 % 10]; /* always: "0.x" */ + buf[3] = digits[n % 10]; +} + +/* Small digits: humidity in %. */ +static void show_small_pct(int n) +{ + buf[1] &= SYM_BAT; + buf[0] = SYM_PERCENT; + if (n > 99) { + buf[0] |= SYM_i; buf[1] |= SYM_H; + return; + } + if (n < 0) { + n = 0; + } + if (n > 9) { + buf[1] |= digits[n / 10]; + } + buf[0] |= digits[n % 10]; +} + +void lcd_show(int valid, s16 t10, int rh, int batt_low) +{ + if (valid) { + show_big_x10(t10); + show_small_pct(rh); + buf[2] = (buf[2] & SYM_BLE) | SYM_DEG_C; + } + if (batt_low) { + buf[1] |= SYM_BAT; + } else { + buf[1] &= ~SYM_BAT; + } + lcd_flush(); +} + +void lcd_show_ble(int on) +{ + if (on) { + buf[2] |= SYM_BLE; + } else { + buf[2] &= ~SYM_BLE; + } + lcd_flush(); +} + +/* Dead-battery screen (as pvvx low_vbat()): battery voltage in the big + * digits ("1.9"), "Lo" in the small ones, battery icon on. */ +void lcd_show_low_battery(u16 mv) +{ + show_big_x10(mv / 100); + buf[1] = SYM_L | SYM_BAT; + buf[0] = SYM_o; + buf[2] = 0; + lcd_flush(); +} diff --git a/src/lcd.h b/src/lcd.h new file mode 100644 index 0000000..ef54162 --- /dev/null +++ b/src/lcd.h @@ -0,0 +1,11 @@ +#pragma once +#include "tl_common.h" + +/* Segment LCD of the LYWSD03MMC 2025 "B1.5" board (pvvx: HW B1.1): + * controller on bit-banged SPI, PD7 CLK / PB7 SDI, write-only. It holds + * the image by itself, so the CPU only talks to it when the content + * changes. */ +void lcd_init(void); /* shows "-- --" */ +void lcd_show(int valid, s16 t10, int rh, int batt_low); +void lcd_show_ble(int on); /* BLE symbol while connected */ +void lcd_show_low_battery(u16 mv); /* "x.x Lo" + battery icon */ diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..88b0c5a --- /dev/null +++ b/src/main.c @@ -0,0 +1,299 @@ +/* + * BTHome v2 temperature / humidity beacon with LCD and OTA - Xiaomi + * LYWSD03MMC B1.5 (Telink TLSR8251, Telink BLE SDK, no RTOS) + * + * Every 60 s: measure T/RH (SHTV3) and VDD, update the segment LCD and the + * BTHome v2 advertisement sent every 10 s. For OTA_WINDOW_S after the + * battery is inserted the advertising is connectable (1 s interval) so the + * Telink OTA service can be reached; after that it is non-connectable. + * + * Power handling follows pvvx/ATC_MiThermometer: + * - no wake-ups of its own: a measurement is started right after every + * 6th advertising event, while the chip is awake anyway + * - the CPU sleeps (suspend) during the ~12 ms sensor conversion instead + * of stalling; the result is collected by an app wake-up timer + * - VDD is sampled while the sensor converts, i.e. under load + * - deep-sleep retention between events, also while connected + * (slave latency 49 -> one connection event per second) + * - battery protection: < 2.2 V at power-on or < 2.0 V while running -> + * "Lo" on the LCD and 3 min deep sleep, before any flash write + */ +#include "tl_common.h" +#include "drivers.h" +#include "stack/ble/ble.h" +#include "vendor/common/blt_common.h" +#include "app_config.h" +#include "sensor.h" +#include "battery.h" +#include "lcd.h" +#include "ble.h" + +#define MEAS_INTERVAL_TICKS ((u32)MEAS_INTERVAL_S * CLOCK_16M_SYS_TIMER_CLK_1S) +#define NAME_LEN (sizeof(DEV_NAME) - 1) + +/* Advertising payload (objects in ascending ID order: 0x0C, 0x2E, 0x45) */ +enum { + OFS_VOLT = 9, /* voltage, 1 mV, uint16 */ + OFS_HUM = 12, /* humidity, 1 %, uint8 */ + OFS_TEMP = 14, /* temperature, 0.1 degC, sint16 */ +}; + +#if ADV_NAME_IN_ADV +#define ADV_LEN (18 + NAME_LEN) +#else +#define ADV_LEN 16 +#endif + +RAM static u8 adv[ADV_LEN] = { + 0x02, 0x01, 0x06, /* flags: LE general, no BR/EDR */ + 0x0C, 0x16, /* service data, 16-bit UUID */ + 0xD2, 0xFC, /* UUID 0xFCD2 */ + 0x40, /* BTHome v2, unencrypted, regular interval */ + 0x0C, 0, 0, /* voltage */ + 0x2E, 0, /* humidity */ + 0x45, 0, 0, /* temperature */ +#if ADV_NAME_IN_ADV + NAME_LEN + 1, 0x09, /* complete local name, filled in at boot */ +#endif +}; +typedef char adv_fits_31_bytes[(sizeof(adv) <= 31) ? 1 : -1]; + +/* Measurement state, kept through deep-sleep retention */ +RAM static u8 adv_count; /* advertising events since last measurement */ +RAM static u8 start_measure; /* measurement due */ +RAM static u8 publish_pending; /* new values to put on air / on the LCD */ +RAM static u8 valid; /* at least one good sensor reading */ +RAM static u32 time_measure; /* != 0: sensor converting since this tick */ +RAM static u32 meas_tick; /* last measurement start (used when connected) */ +RAM static s16 t10; +RAM static u8 rh_pct; +RAM static u16 vbat_mv; + +static inline void put_le16(u8 *p, u16 v) +{ + p[0] = v & 0xFF; + p[1] = v >> 8; +} + +/* Deep sleep without retention: the next wake-up is a fresh boot */ +static void deep_sleep_s(u32 s) +{ + cpu_sleep_wakeup(DEEPSLEEP_MODE, PM_WAKEUP_TIMER, + clock_time() + s * CLOCK_16M_SYS_TIMER_CLK_1S); + while (1) { + } +} + +/* pvvx low_vbat(): stop everything, show it, retry in 3 minutes */ +static void low_battery(u16 mv) +{ + sensor_sleep(); + lcd_show_low_battery(mv); + deep_sleep_s(LOW_VBAT_SLEEP_S); +} + +_attribute_ram_code_ +static void store_result(s32 mc, s32 mrh) +{ + s32 pct = (mrh + 500) / 1000; + + t10 = (mc + (mc < 0 ? -50 : 50)) / 100; + rh_pct = pct < 0 ? 0 : (pct > 100 ? 100 : pct); + valid = 1; +} + +/* App wake-up timer: the sensor conversion is finished (pvvx + * WakeupLowPowerCb). Also called from main_loop() if the stack did not + * sleep at all. A failed read keeps the last good T/RH values. */ +_attribute_ram_code_ +static void measure_done_cb(int unused) +{ + s32 mc, mrh; + + (void)unused; + if (!time_measure) { + return; + } + time_measure = 0; + bls_pm_setAppWakeupLowPower(0, 0); + if (!sensor_result(&mc, &mrh)) { + store_result(mc, mrh); + } + publish_pending = 1; +} + +/* Start a measurement: trigger the sensor, read VDD while it converts, + * then let the stack sleep until the result is ready. */ +_attribute_ram_code_ +static void measure_start(void) +{ + meas_tick = clock_time(); + if (!sensor_start()) { + time_measure = clock_time() | 1; + } + vbat_mv = battery_read_mv(); + if (vbat_mv < END_VBAT_MV) { + low_battery(vbat_mv); + } + if (time_measure) { + bls_pm_setAppWakeupLowPower(time_measure + SENSOR_MEAS_TICKS, 1); + } else { + publish_pending = 1; /* sensor absent: voltage only */ + } +} + +_attribute_ram_code_ +static void publish(void) +{ + publish_pending = 0; + if (valid) { + put_le16(&adv[OFS_TEMP], (u16)t10); + adv[OFS_HUM] = rh_pct; + } + put_le16(&adv[OFS_VOLT], vbat_mv); + bls_ll_setAdvData(adv, sizeof(adv)); + lcd_show(valid, t10, rh_pct, vbat_mv < LOW_BATT_MV); +} + +/* Called by the stack right before every advertising event (pvvx + * app_advertise_prepare_handler): count events, flag a measurement on + * every MEAS_EVERY_N_ADV-th one. The measurement itself starts in + * main_loop() once the event is done. While the OTA window runs at the + * fast interval, main_loop() uses a timer instead. */ +_attribute_ram_code_ +static int adv_prepare_cb(void *p) +{ + (void)p; + if (!ota_window && ++adv_count >= MEAS_EVERY_N_ADV) { + adv_count = 0; + start_measure = 1; + } + return 1; /* send this advertising packet */ +} + +static void user_init_normal(void) +{ + blc_ll_initBasicMCU(); + adc_power_on_sar_adc(0); + lpc_power_down(); + + /* Analog pull resistors are kept through deep sleep: set once */ + gpio_setup_up_down_resistor(I2C_SCL, PULL_I2C); + gpio_setup_up_down_resistor(I2C_SDA, PULL_I2C); + gpio_setup_up_down_resistor(GPIO_LCD_PB6, PULL_LCD_PB6); + gpio_setup_up_down_resistor(GPIO_LCD_CLK, PULL_LCD_SER); + gpio_setup_up_down_resistor(GPIO_LCD_SDI, PULL_LCD_SER); + gpio_setup_up_down_resistor(GPIO_KEY, PULL_KEY); + + /* Battery first: nothing below may write flash on a dying cell + * (pvvx start_tst_battery) */ + vbat_mv = battery_read_mv(); + if (vbat_mv < BOOT_VBAT_MV) { + sensor_init(); /* idle -> sleep */ + lcd_init(); + low_battery(vbat_mv); + } + + /* The library writes flash for OTA: clear write protection left by + * other firmware (as pvvx, once per power-on) */ + flash_unlock(); + random_generator_init(); + +#if ADV_NAME_IN_ADV + memcpy(&adv[18], DEV_NAME, NAME_LEN); +#endif + lcd_init(); /* shows "-- --" until the first good reading */ + sensor_init(); /* put the SHTV3 to sleep */ + + /* First measurement, blocking, so the first packet carries data */ + if (!sensor_start()) { + s32 mc, mrh; + + vbat_mv = battery_read_mv(); + pm_wait_us(SENSOR_MEAS_US); + if (!sensor_result(&mc, &mrh)) { + store_result(mc, mrh); + } + } + publish(); + + /* DEEP_ANA_REG0 survives deep sleep and software reboots and is + * cleared only by a power cycle (pvvx uses it the same way) */ + int power_on = analog_read(DEEP_ANA_REG0) != 0x55; + + analog_write(DEEP_ANA_REG0, 0x55); + + bls_pm_registerAppWakeupLowPowerCb(measure_done_cb); + ble_init(adv, sizeof(adv), adv_prepare_cb, power_on); + meas_tick = clock_time(); +} + +_attribute_ram_code_ +static void user_init_deepRetn(void) +{ + blc_ll_initBasicMCU(); + ble_init_deep_retention(); +} + +_attribute_ram_code_ +static void main_loop(void) +{ + blt_sdk_main_loop(); + + if (ble_task()) { + return; /* OTA in progress: no measurements */ + } + + if (time_measure && + clock_time() - time_measure > SENSOR_MEAS_TICKS) { + measure_done_cb(0); /* stack did not sleep in between */ + } + + /* While connected there are no advertising events to count, and in + * the OTA window they come every second */ + if ((ble_connected || ota_window) && + clock_time() - meas_tick >= MEAS_INTERVAL_TICKS) { + start_measure = 1; + } + + if (start_measure && !time_measure) { + start_measure = 0; + measure_start(); + } + + if (publish_pending) { + publish(); + } +} + +_attribute_ram_code_ +void irq_handler(void) +{ + irq_blt_sdk_handler(); +} + +_attribute_ram_code_ +int main(void) +{ + blc_pm_select_internal_32k_crystal(); /* no 32 kHz crystal fitted */ + cpu_wakeup_init(); + + int deep_ret = pm_is_MCU_deepRetentionWakeup(); + + gpio_init(!deep_ret); + clock_init(SYS_CLK_TYPE); + rf_drv_init(RF_MODE_BLE_1M); + reg_clk_en0 = FLD_CLK0_SWIRE_EN; /* gate unused peripheral clocks */ + blc_app_loadCustomizedParameters(); /* factory crystal trim */ + + if (deep_ret) { + user_init_deepRetn(); + } else { + user_init_normal(); + } + + irq_enable(); + for (;;) { + main_loop(); + } +} diff --git a/src/sensor.c b/src/sensor.c new file mode 100644 index 0000000..0cb097d --- /dev/null +++ b/src/sensor.c @@ -0,0 +1,94 @@ +#include "tl_common.h" +#include "drivers.h" +#include "app_config.h" +#include "i2c.h" +#include "sensor.h" + +/* SHTV3 / SHTC3 on the B1.5 board */ +#define SHTC3_ADDR 0x70 +#define SHTC3_WAKEUP_US 240 + +static const u8 shtc3_wakeup[] = { 0x35, 0x17 }; +static const u8 shtc3_sleep[] = { 0xB0, 0x98 }; +static const u8 shtc3_meas[] = { 0x78, 0x66 }; /* normal mode, T first, no stretching */ + +_attribute_ram_code_ +static u8 crc8(const u8 *p, int len) +{ + u8 crc = 0xFF; + + while (len--) { + crc ^= *p++; + for (int i = 0; i < 8; i++) { + crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : (crc << 1); + } + } + return crc; +} + +/* Wake the sensor once and put it straight back to sleep (~0.3 uA). + * It powers up in idle mode, which draws far more than sleep. */ +int sensor_init(void) +{ + int err; + + i2c_bus_init(); + err = i2c_write(SHTC3_ADDR, shtc3_wakeup, sizeof(shtc3_wakeup)); + if (!err) { + sleep_us(SHTC3_WAKEUP_US); + i2c_write(SHTC3_ADDR, shtc3_sleep, sizeof(shtc3_sleep)); + } + return err; +} + +_attribute_ram_code_ +int sensor_start(void) +{ + int err; + + i2c_bus_init(); + err = i2c_write(SHTC3_ADDR, shtc3_wakeup, sizeof(shtc3_wakeup)); + if (err) { + return err; + } + sleep_us(SHTC3_WAKEUP_US); + err = i2c_write(SHTC3_ADDR, shtc3_meas, sizeof(shtc3_meas)); + if (err) { + i2c_write(SHTC3_ADDR, shtc3_sleep, sizeof(shtc3_sleep)); + } + return err; +} + +/* Raw 16-bit words -> milli-units. The x/65536 scaling is done as + * (x * k) >> 13, which fits in 32 bits: + * 175000 / 8 = 21875, 100000 / 8 = 12500 */ +_attribute_ram_code_ +int sensor_result(s32 *t_mc, s32 *rh_mpct) +{ + u8 buf[6]; + int err; + + i2c_bus_init(); /* I2C registers do not survive deep-sleep retention */ + err = i2c_read(SHTC3_ADDR, buf, sizeof(buf)); + i2c_write(SHTC3_ADDR, shtc3_sleep, sizeof(shtc3_sleep)); + + if (err || crc8(&buf[0], 2) != buf[2] || crc8(&buf[3], 2) != buf[5]) { + return -1; + } + + u32 t_raw = (buf[0] << 8) | buf[1]; + u32 h_raw = (buf[3] << 8) | buf[4]; + + *t_mc = (s32)((t_raw * 21875) >> 13) - 45000; + *rh_mpct = (s32)((h_raw * 12500) >> 13); + return 0; +} + +/* The sleep command is NAKed while a conversion is running, so allow + * for one to finish first. */ +void sensor_sleep(void) +{ + i2c_bus_init(); + sleep_us(SENSOR_MEAS_US); + i2c_write(SHTC3_ADDR, shtc3_sleep, sizeof(shtc3_sleep)); +} diff --git a/src/sensor.h b/src/sensor.h new file mode 100644 index 0000000..a01076d --- /dev/null +++ b/src/sensor.h @@ -0,0 +1,18 @@ +#pragma once +#include "tl_common.h" + +/* Temperature/humidity sensor of the LYWSD03MMC B1.5: + * SHTV3 (SHTC3-compatible) at I2C address 0x70. + * + * A measurement is split in two so the CPU can sleep while the sensor + * converts (as pvvx SENSOR_SLEEP_MEASURE does): + * sensor_start() wake the sensor and trigger a conversion + * ... SENSOR_MEAS_TICKS later ... + * sensor_result() read the result and put the sensor back to sleep */ +#define SENSOR_MEAS_US 12500 /* SHTC3 normal mode: max 12.1 ms */ +#define SENSOR_MEAS_TICKS (SENSOR_MEAS_US * CLOCK_16M_SYS_TIMER_CLK_1US) + +int sensor_init(void); /* 0 = sensor answered */ +int sensor_start(void); /* 0 = conversion started */ +int sensor_result(s32 *t_mc, s32 *rh_mpct); /* milli-degC, milli-%; 0 = ok */ +void sensor_sleep(void); /* force sleep (e.g. low battery) */