diff --git a/libraries/Nicla_System/examples/NiclaSenseME_BatteryChargingSimple/NiclaSenseME_BatteryChargingSimple.ino b/libraries/Nicla_System/examples/NiclaSenseME_BatteryChargingSimple/NiclaSenseME_BatteryChargingSimple.ino new file mode 100644 index 000000000..10e4cabfc --- /dev/null +++ b/libraries/Nicla_System/examples/NiclaSenseME_BatteryChargingSimple/NiclaSenseME_BatteryChargingSimple.ino @@ -0,0 +1,90 @@ +/* + * This example shows how to use the Nicla Sense ME library to charge a battery. + * + * The LED color will change depending on the battery's operating status: + * - Blue: Ready + * - Yellow: Charging + * - Green: Charging complete + * - Red: Error + * + * Instructions: + * 1. Connect a single cell (3.7V nominal) LiPo/Li-Ion battery to the board via battery connector (J4) or the pin header (J3). + * 2. Configure the charge current in the setup() function. + * 3. Upload this sketch to your Nicla Sense ME board. + * + * Initial author: Sebastian Romero @sebromero + */ + +#include "Nicla_System.h" + +constexpr auto printInterval { 10000ul }; +float voltage = -1.0f; + +void setup(){ + Serial.begin(115200); + for (const auto timeout = millis() + 2500; millis() < timeout && !Serial; delay(250)); + nicla::begin(); // Initialise library + nicla::leds.begin(); // Start I2C connection to LED driver + nicla::setBatteryNTCEnabled(true); // Set to false if your battery doesn't have a NTC. + + /* + Set the maximum charging time to 9 hours. This helps to prevent overcharging. + Set this to a lower value (e.g. 3h) if your battery will be done with charging sooner. + To get an estimation of the charging time, you can use the following formula: + Charging time (in hours) = (Battery capacity in mAh) / (0.8 * Charging current in mA) + This formula takes into account that the charging process is approximately 80% efficient (hence the 0.8 factor). + This is just a rough estimate, and actual charging time may vary depending on factors like the charger, battery quality, and charging conditions. + */ + nicla::configureChargingSafetyTimer(ChargingSafetyTimerOption::NineHours); + + /* + A safe default charging current value that works for most common LiPo batteries is 0.5C, + which means charging at a rate equal to half of the battery's capacity. + For example, a 200mAh battery could be charged at 100mA (0.1A). + */ + nicla::enableCharging(100); + + nicla::leds.setColor(blue); +} + +void loop(){ + + static auto updateTimestamp = millis(); + + if (millis() - updateTimestamp >= printInterval) { + updateTimestamp = millis(); + + float currentVoltage = nicla::getCurrentBatteryVoltage(); + if(currentVoltage != voltage){ + voltage = currentVoltage; + Serial.print("\nVoltage: "); + Serial.println(voltage); + } else { + Serial.print("."); + } + + auto operatingStatus = nicla::getOperatingStatus(); + + switch(operatingStatus) { + case OperatingStatus::Charging: + nicla::leds.setColor(255,100,0); // Yellow + break; + case OperatingStatus::ChargingComplete: + nicla::leds.setColor(green); + + // This will stop further charging until enableCharging() is called again. + nicla::disableCharging(); + break; + case OperatingStatus::Error: + nicla::leds.setColor(red); + break; + case OperatingStatus::Ready: + nicla::leds.setColor(blue); + break; + default: + nicla::leds.setColor(off); + break; + } + + } +} diff --git a/libraries/Nicla_System/examples/NiclaSenseME_BatteryStatus/NiclaSenseME_BatteryStatus.ino b/libraries/Nicla_System/examples/NiclaSenseME_BatteryStatus/NiclaSenseME_BatteryStatus.ino new file mode 100644 index 000000000..57b2cf8e9 --- /dev/null +++ b/libraries/Nicla_System/examples/NiclaSenseME_BatteryStatus/NiclaSenseME_BatteryStatus.ino @@ -0,0 +1,338 @@ +/* + * This example shows how to use the Nicla Sense ME library to read the battery status and send it over BLE. + * + * When not connected over BLE, the battery status is printed over the serial port every 4 seconds. + * When connected over BLE, the battery status is checked every 4 seconds and sent over BLE if a value has changed. + * That is, when using the notification mechanism in the BatteryMonitor web app, which is the default. + * The BatteryMonitor web app can also be configured to poll the battery status every X seconds. + * + * The LED colors are used to indicate the BLE connection status: + * - Green: Board is ready but no BLE connection has been established yet. + * - Blue: Board is connected over BLE to a device that wants to read the battery status. + * - Red: A device that was previously connected over BLE has disconnected. + * + * Instructions: + * 1. Upload this sketch to your Nicla Sense ME board. + * 2. Open the BatteryMonitor web app (index.html) in a browser that supports Web Bluetooth (Chrome, Edge, Opera, etc.). + * 3. Connect to your Nicla Sense ME board by clicking the "Connect" button. + * + * Initial author: Sebastian Romero @sebromero + */ + +#include "Nicla_System.h" +#include + +constexpr auto printInterval { 4000ul }; +constexpr auto batteryUpdateInterval { 4000ul }; +int8_t batteryChargeLevel = -1; +int8_t batteryPercentage = -1; +float batteryVoltage = -1.0f; +int8_t runsOnBattery = -1; // Using an int to be able to represent an unknown state. +int8_t batteryIsCharging = -1; // Using an int to be able to represent an unknown state. + + +#define DEVICE_NAME "NiclaSenseME" +#define DEVICE_UUID(val) ("19b10000-" val "-537e-4f6c-d104768a1214") +BLEService service(DEVICE_UUID("0000")); + +BLEIntCharacteristic batteryPercentageCharacteristic(DEVICE_UUID("1001"), BLERead | BLENotify); +BLEFloatCharacteristic batteryVoltageCharacteristic(DEVICE_UUID("1002"), BLERead | BLENotify); +BLEIntCharacteristic batteryChargeLevelCharacteristic(DEVICE_UUID("1003"), BLERead | BLENotify); +BLEBooleanCharacteristic runsOnBatteryCharacteristic(DEVICE_UUID("1004"), BLERead | BLENotify); +BLEBooleanCharacteristic isChargingCharacteristic(DEVICE_UUID("1005"), BLERead | BLENotify); + +bool updateBatteryStatus(){ + static auto updateTimestamp = millis(); + bool intervalFired = millis() - updateTimestamp >= batteryUpdateInterval; + bool isFirstReading = runsOnBattery == -1 || batteryIsCharging == -1; + + if (intervalFired || isFirstReading) { + Serial.println("Checking the battery status..."); + updateTimestamp = millis(); + int8_t isCharging = nicla::getOperatingStatus() == OperatingStatus::Charging; + int8_t batteryPowered = nicla::runsOnBattery(); + bool valueUpdated = false; + + if (batteryIsCharging != isCharging) { + batteryIsCharging = isCharging; + valueUpdated = true; + } + + if (runsOnBattery != batteryPowered) { + runsOnBattery = batteryPowered; + valueUpdated = true; + } + + return valueUpdated; + } + + return false; +} + +bool updateBatteryLevel() { + static auto updateTimestamp = millis(); + bool intervalFired = millis() - updateTimestamp >= batteryUpdateInterval; + bool isFirstReading = batteryPercentage == -1 || batteryVoltage == -1.0f; + + if (intervalFired || isFirstReading) { + Serial.println("Checking the battery level..."); + updateTimestamp = millis(); + auto percentage = nicla::getBatteryVoltagePercentage(); + + if (percentage < 0) { + Serial.println("Battery voltage percentage couldn't be determined."); + return false; + } + + // Only if the percentage has changed, we update the values as they depend on it. + if (batteryPercentage != percentage) { + int8_t currentChargeLevel = static_cast(nicla::getBatteryChargeLevel()); + if(currentChargeLevel == 0){ + Serial.println("Battery charge level couldn't be determined."); + return false; + } + + auto currentVoltage = nicla::getCurrentBatteryVoltage(); + if(currentVoltage == 0){ + Serial.println("Battery voltage couldn't be determined."); + return false; + } + + batteryPercentage = percentage; + batteryChargeLevel = currentChargeLevel; + batteryVoltage = currentVoltage; + + return true; + } + } + + return false; +} + +String getBatteryTemperatureDescription(BatteryTemperature status) { + switch (status) { + case BatteryTemperature::Normal: + return "Normal"; + case BatteryTemperature::Extreme: + return "Extreme"; + case BatteryTemperature::Cool: + return "Cool"; + case BatteryTemperature::Warm: + return "Warm"; + default: + return "Unknown"; + } +} + +String getBatteryChargeLevelDescription(BatteryChargeLevel status) { + switch (status) { + case BatteryChargeLevel::Empty: + return "Empty"; + case BatteryChargeLevel::AlmostEmpty: + return "Almost Empty"; + case BatteryChargeLevel::HalfFull: + return "Half Full"; + case BatteryChargeLevel::AlmostFull: + return "Almost Full"; + case BatteryChargeLevel::Full: + return "Full"; + default: + return "Unknown"; + } +} + + +void blePeripheralDisconnectHandler(BLEDevice central) { + nicla::leds.setColor(red); + Serial.println("Device disconnected."); +} + +void blePeripheralConnectHandler(BLEDevice central) { + nicla::leds.setColor(blue); + Serial.println("Device connected."); +} + +void onBatteryVoltageCharacteristicRead(BLEDevice central, BLECharacteristic characteristic) { + Serial.println("Requesting battery voltage..."); + updateBatteryLevel(); + Serial.print("Battery voltage: "); + Serial.println(batteryVoltage); + batteryVoltageCharacteristic.writeValue(batteryVoltage); +} + +void onBatteryPercentageCharacteristicRead(BLEDevice central, BLECharacteristic characteristic) { + Serial.println("Requesting battery percentage..."); + updateBatteryLevel(); + Serial.print("Battery Percent: "); + Serial.println(batteryPercentage); + batteryPercentageCharacteristic.writeValue(batteryPercentage); +} + +void onBatteryChargeLevelCharacteristicRead(BLEDevice central, BLECharacteristic characteristic) { + Serial.println("Requesting battery charge level..."); + updateBatteryLevel(); + Serial.print("Battery Charge Level: "); + Serial.println(batteryChargeLevel); + batteryChargeLevelCharacteristic.writeValue(batteryChargeLevel); +} + +void onRunsOnBatteryCharacteristicRead(BLEDevice central, BLECharacteristic characteristic) { + Serial.println("Checking if device runs on battery..."); + updateBatteryStatus(); + Serial.print("Runs on battery: "); + Serial.println(runsOnBattery == 1 ? "Yes" : "No"); + runsOnBatteryCharacteristic.writeValue(runsOnBattery == 1); +} + +void onIsChargingCharacteristicRead(BLEDevice central, BLECharacteristic characteristic) { + Serial.println("Checking if battery is charging..."); + updateBatteryStatus(); + Serial.print("Battery is charging: "); + Serial.println(batteryIsCharging == 1 ? "Yes" : "No"); + isChargingCharacteristic.writeValue(batteryIsCharging == 1); +} + +void onCharacteristicSubscribed(BLEDevice central, BLECharacteristic characteristic) { + Serial.println("Device subscribed to characteristic: " + String(characteristic.uuid())); +} + +void setupBLE() { + if (!BLE.begin()) { + Serial.println("Failed to initialized BLE!"); + + while (true) { + // Blink the red LED to indicate failure + nicla::leds.setColor(red); + delay(500); + nicla::leds.setColor(off); + delay(500); + } + } + + BLE.setLocalName(DEVICE_NAME); + BLE.setDeviceName(DEVICE_NAME); + BLE.setAdvertisedService(service); + BLE.setEventHandler(BLEDisconnected, blePeripheralDisconnectHandler); + BLE.setEventHandler(BLEConnected, blePeripheralConnectHandler); + + service.addCharacteristic(batteryPercentageCharacteristic); + batteryPercentageCharacteristic.setEventHandler(BLERead, onBatteryPercentageCharacteristicRead); + batteryPercentageCharacteristic.setEventHandler(BLESubscribed, onCharacteristicSubscribed); + batteryPercentageCharacteristic.writeValue(batteryPercentage); + + service.addCharacteristic(batteryVoltageCharacteristic); + batteryVoltageCharacteristic.setEventHandler(BLERead, onBatteryVoltageCharacteristicRead); + batteryVoltageCharacteristic.setEventHandler(BLESubscribed, onCharacteristicSubscribed); + batteryVoltageCharacteristic.writeValue(batteryVoltage); + + service.addCharacteristic(batteryChargeLevelCharacteristic); + batteryChargeLevelCharacteristic.setEventHandler(BLERead, onBatteryChargeLevelCharacteristicRead); + batteryChargeLevelCharacteristic.setEventHandler(BLESubscribed, onCharacteristicSubscribed); + batteryChargeLevelCharacteristic.writeValue(batteryChargeLevel); + + service.addCharacteristic(runsOnBatteryCharacteristic); + runsOnBatteryCharacteristic.setEventHandler(BLERead, onRunsOnBatteryCharacteristicRead); + runsOnBatteryCharacteristic.setEventHandler(BLESubscribed, onCharacteristicSubscribed); + runsOnBatteryCharacteristic.writeValue(runsOnBattery == 1); + + service.addCharacteristic(isChargingCharacteristic); + isChargingCharacteristic.setEventHandler(BLERead, onIsChargingCharacteristicRead); + isChargingCharacteristic.setEventHandler(BLESubscribed, onCharacteristicSubscribed); + isChargingCharacteristic.writeValue(batteryIsCharging == 1); + + BLE.addService(service); + BLE.advertise(); +} + +void setup() +{ + Serial.begin(115200); + for (const auto timeout = millis() + 2500; millis() < timeout && !Serial; delay(250)); + + // run this code once when Nicla Sense ME board turns on + nicla::begin(); // initialise library + nicla::leds.begin(); // Start I2C connection + + nicla::setBatteryNTCEnabled(true); // Set to false if your battery doesn't have an NTC thermistor. + nicla::enableCharging(); + + nicla::leds.setColor(green); + + setupBLE(); +} + +void loop() +{ + //BLE.poll(); // Implicit when calling BLE.connected(). Uncomment when only using BLERead (polling mechanism) + + // Check if a BLE device is connected and handle battery updates + // via the notification mechanism. + if (BLE.connected()) { + bool newBatteryLevelAvailable = updateBatteryLevel(); + bool newBatteryStatusAvailable = updateBatteryStatus(); + + if (batteryPercentageCharacteristic.subscribed() && newBatteryLevelAvailable) { + Serial.print("Battery Percentage: "); + Serial.println(batteryPercentage); + batteryPercentageCharacteristic.writeValue(batteryPercentage); + } + + if (batteryVoltageCharacteristic.subscribed() && newBatteryLevelAvailable) { + Serial.print("Battery Voltage: "); + Serial.println(batteryVoltage); + batteryVoltageCharacteristic.writeValue(batteryVoltage); + } + + if (batteryChargeLevelCharacteristic.subscribed() && newBatteryLevelAvailable) { + Serial.print("Battery charge level: "); + Serial.println(batteryChargeLevel); + batteryChargeLevelCharacteristic.writeValue(batteryChargeLevel); + } + + if(runsOnBatteryCharacteristic.subscribed() && newBatteryStatusAvailable) { + Serial.print("Runs on battery: "); + Serial.println(runsOnBattery == 1 ? "Yes" : "No"); + runsOnBatteryCharacteristic.writeValue(runsOnBattery == 1); + } + + if(isChargingCharacteristic.subscribed() && newBatteryStatusAvailable) { + Serial.print("Battery is charging: "); + Serial.println(batteryIsCharging == 1 ? "Yes" : "No"); + isChargingCharacteristic.writeValue(batteryIsCharging == 1); + } + + return; + } + + static auto updateTimestamp = millis(); + + if (millis() - updateTimestamp >= printInterval) { + updateTimestamp = millis(); + + float voltage = nicla::getCurrentBatteryVoltage(); + Serial.print("Voltage: "); + Serial.println(voltage); + + Serial.print("Battery Percent: "); + auto percent = nicla::getBatteryVoltagePercentage(); + Serial.println(percent); + + Serial.print("Battery Temperature: "); + auto temperature = nicla::getBatteryTemperature(); + Serial.println(getBatteryTemperatureDescription(temperature)); + + auto chargeLevel = nicla::getBatteryChargeLevel(); + Serial.println("Battery is " + getBatteryChargeLevelDescription(chargeLevel)); + + bool isCharging = nicla::getOperatingStatus() == OperatingStatus::Charging; + Serial.print("Battery is charging: "); + Serial.println(isCharging ? "Yes" : "No"); + + bool runsOnBattery = nicla::runsOnBattery(); + Serial.print("Runs on battery: "); + Serial.println(runsOnBattery ? "Yes" : "No"); + + + Serial.println("----------------------"); + } +} diff --git a/libraries/Nicla_System/extras/BatteryMonitor/app.js b/libraries/Nicla_System/extras/BatteryMonitor/app.js new file mode 100644 index 000000000..8baf51923 --- /dev/null +++ b/libraries/Nicla_System/extras/BatteryMonitor/app.js @@ -0,0 +1,207 @@ +/** + * This is a simple example of a web app that connects to an Arduino board + * and reads the battery level and other battery related characteristics. + * + * It uses the Web Bluetooth API to connect to the Arduino board. + * + * Instructions: + * 1. Upload the NiclaSenseME_BatteryStatus sketch to the Arduino board. + * 2. Open the index.html file in a browser that supports the Web Bluetooth API (Chrome, Edge, Opera). + * 3. Click on the Connect button to connect to the Arduino board. + * + * Initial author: Sebastian Romero @sebromero + */ + + +/// UI elements +const connectButton = document.getElementById('connect'); +const batteryLevelElement = document.getElementById('battery-level'); +const batteryLabel = document.getElementById('battery-label'); +const chargingIconElement = document.getElementById('charging-icon'); +const externalPowerIconElement = document.getElementById('external-powered-icon'); + +const serviceUuid = '19b10000-0000-537e-4f6c-d104768a1214'; +let pollIntervalID; +let peripheralDevice; + +/// Data structure to hold the characteristics and their values plus data conversion functions. +let data = { + "batteryPercentage": { + "name": "Battery Percentage", + "value": 0, + "unit": "%", + "characteristic": null, + "characteristicUUID": "19b10000-1001-537e-4f6c-d104768a1214", + "extractData": function(dataView) { + return dataView.getInt8(0); + } + }, + "batteryVoltage": { + "name": "Battery Voltage", + "value": 0, + "unit": "V", + "characteristic": null, + "characteristicUUID": "19b10000-1002-537e-4f6c-d104768a1214", + "extractData": function(dataView) { + return dataView.getFloat32(0, true); + } + }, + "batteryChargeLevel": { + "name": "Battery Charge Level", + "value": 0, + "unit": "", + "characteristic": null, + "characteristicUUID": "19b10000-1003-537e-4f6c-d104768a1214", + "extractData": function(dataView) { + return dataView.getInt8(0); + }, + "getColor": function(value) { + // Red to green range with 5 steps and white for the unknown state + const colors = ["#ffffff", "#ff2d2d", "#fc9228", "#ffea00", "#adfd5c", "#00c600"]; + return colors[value]; + } + }, + "runsOnBattery": { + "name": "Runs on Battery", + "value": false, + "unit": "", + "characteristic": null, + "characteristicUUID": "19b10000-1004-537e-4f6c-d104768a1214", + "extractData": function(dataView) { + return dataView.getUint8(0) == 1; + } + }, + + "isCharging": { + "name": "Is Charging", + "value": false, + "unit": "", + "characteristic": null, + "characteristicUUID": "19b10000-1005-537e-4f6c-d104768a1214", + "extractData": function(dataView) { + return dataView.getUint8(0) == 1; + } + } +}; + +function onDisconnected(event) { + let device = event.target; + connectButton.disabled = false; + connectButton.style.opacity = 1; + if(pollIntervalID) clearInterval(pollIntervalID); + console.log(`Device ${device.name} is disconnected.`); + + // Reset the battery level display + batteryLevelElement.style.width = "0px"; + batteryLabel.textContent = ""; + chargingIconElement.style.display = "none"; + externalPowerIconElement.style.display = "none"; +} + +/** + * Connects to the Arduino board and starts reading the characteristics. + * @param {Boolean} usePolling The default is to use notifications, but polling can be used instead. + * In that case a poll interval can be defined. + * @param {Number} pollInterval The interval in milliseconds to poll the characteristics from the device. + */ +async function connectToPeripheralDevice(usePolling = false, pollInterval = 5000){ + if (peripheralDevice && peripheralDevice.gatt.connected) { + console.log("Already connected"); + return; + } + + peripheralDevice = await navigator.bluetooth.requestDevice({ + filters: [{ services: [serviceUuid] }] + }); + peripheralDevice.addEventListener('gattserverdisconnected', onDisconnected); + + const server = await peripheralDevice.gatt.connect(); + console.log("Connected to: " + peripheralDevice.name); + const service = await server.getPrimaryService(serviceUuid); + + await Promise.all( + Object.keys(data).map(async (key) => { + let item = data[key]; + const characteristic = await service.getCharacteristic(item.characteristicUUID); + item.characteristic = characteristic; + + if (!usePolling) { + characteristic.addEventListener('characteristicvaluechanged', handleCharacteristicChange); + characteristic.readValue(); // Perform an initial read + await characteristic.startNotifications(); + } + }) + ); + + if (usePolling) { + pollIntervalID = setInterval(readCharacteristicsData, pollInterval); + await readCharacteristicsData(); + } +} + +connectButton.addEventListener('click', async () => { + try { + await connectToPeripheralDevice(); + connectButton.disabled = true; + connectButton.style.opacity = 0.5; + } catch (error) { + if(error.message === "User cancelled the requestDevice() chooser."){ + return; + } + + console.error('Error:', error); + connectButton.style.backgroundColor = "red"; + } +}); + +/** + * Renders the data from the device in the UI. + * It displays the battery level as a visual bar color coded from red to green. + * It also displays the battery voltage and the percentage of the regulated voltage. + * It also displays the charging and external power status. + */ +function displayBatteryData() { + const batteryPercentage = data.batteryPercentage.value; + const batteryVoltage = data.batteryVoltage.value; + const regulatedVoltage = (batteryVoltage / batteryPercentage * 100).toFixed(2); + + // Map the range from 0-5 to 0-100 + const batteryPercentageMapped = data.batteryChargeLevel.value * 20; + batteryLevelElement.style.width = `${batteryPercentageMapped * 0.56}px`; // Scale the battery level to the width of the battery div + batteryLevelElement.style.backgroundColor = data.batteryChargeLevel.getColor(data.batteryChargeLevel.value); + batteryLabel.textContent = `${batteryVoltage.toFixed(2)}V (${batteryPercentage}% of ${regulatedVoltage}V)`; + + chargingIconElement.style.display = data.isCharging.value ? "block" : "none"; + externalPowerIconElement.style.display = data.runsOnBattery.value ? "none" : "block"; +} + +/** + * Used together with polling to read the characteristics from the device. + * After reading the data it is displayed in the UI by calling displayBatteryData(). + */ +async function readCharacteristicsData() { + await Promise.all( + Object.keys(data).map(async (key) => { + let item = data[key]; + console.log("Requesting " + item.name + "..."); + item.value = item.extractData(await item.characteristic.readValue()); + console.log(item.name + ": " + item.value + item.unit); + }) + ); + displayBatteryData(); +} + +/** + * Callback function that is called when a characteristic value changes. + * Updates the data object with the new value and displays it in the UI by calling displayBatteryData(). + * @param {*} event The event that contains the characteristic that changed. + */ +function handleCharacteristicChange(event) { + // Find the characteristic that changed in the data object by matching the UUID + let dataItem = Object.values(data).find(item => item.characteristicUUID === event.target.uuid); + let dataView = event.target.value; + dataItem.value = dataItem.extractData(dataView); + + console.log(`'${dataItem.name}' changed: ${dataItem.value}${dataItem.unit}`); + displayBatteryData(); +} diff --git a/libraries/Nicla_System/extras/BatteryMonitor/index.html b/libraries/Nicla_System/extras/BatteryMonitor/index.html new file mode 100644 index 000000000..cef2c7524 --- /dev/null +++ b/libraries/Nicla_System/extras/BatteryMonitor/index.html @@ -0,0 +1,38 @@ + + + + + + WebBLE Battery Monitor + + + + + + + +

WebBLE Battery Monitor 🔋

+
+
+
+
+

+
+
+ + + + +
+
+ + + +
+
+ +
+ + diff --git a/libraries/Nicla_System/extras/BatteryMonitor/style.css b/libraries/Nicla_System/extras/BatteryMonitor/style.css new file mode 100644 index 000000000..fe302e9d6 --- /dev/null +++ b/libraries/Nicla_System/extras/BatteryMonitor/style.css @@ -0,0 +1,70 @@ +:root { + --main-control-color: #008184; + --main-control-color-hover: #005c5f; + --main-flexbox-gap: 16px; + --secondary-text-color: #87898b; +} + +body { + font-family: 'Open Sans', sans-serif; + text-align: center; +} + +.container { + display: inline-block; + margin-top: 20px; +} + +button { + font-family: 'Open Sans', sans-serif; + font-weight: 700; + font-size: 1rem; + justify-content: center; + background-color: var(--main-control-color); + color: #fff; + cursor: pointer; + letter-spacing: 1.28px; + line-height: normal; + outline: none; + padding: 8px 18px; + text-align: center; + text-decoration: none; + border: 2px solid transparent; + border-radius: 32px; + text-transform: uppercase; + box-sizing: border-box; +} + +button:hover { + background-color: var(--main-control-color-hover); +} + +.battery { + width: 60px; + height: 30px; + border: 2px solid #999; + border-radius: 5px; + position: relative; + margin: 20px auto; +} + +.battery-level { + position: absolute; + bottom: 2px; + left: 2px; + background-color: green; + border-radius: 2px; + width: 0; + height: 26px; +} + +#battery-status { + display: flex; + flex-direction: row; + margin: 20px 0; + justify-content: center; +} + +#battery-status > div { + display: none; +} \ No newline at end of file diff --git a/libraries/Nicla_System/src/BQ25120A.cpp b/libraries/Nicla_System/src/BQ25120A.cpp new file mode 100644 index 000000000..784970912 --- /dev/null +++ b/libraries/Nicla_System/src/BQ25120A.cpp @@ -0,0 +1,88 @@ +#include +#include +#include "BQ25120A.h" +#include "Nicla_System.h" +#include "DigitalOut.h" + +// Set the CD pin to low to enter high impedance mode +// Note that this only applies when powered with a battery +// and the condition VIN < VUVLO is met. +// When VIN > VUVLO this enables charging. +static mbed::DigitalOut cd(p25, 0); + +uint8_t BQ25120A::getStatusRegister() +{ + return readByte(BQ25120A_ADDRESS, BQ25120A_STATUS); +} + +uint8_t BQ25120A::getFaultsRegister() +{ + return readByte(BQ25120A_ADDRESS, BQ25120A_FAULTS); +} + +uint8_t BQ25120A::getFastChargeControlRegister() +{ + return readByte(BQ25120A_ADDRESS, BQ25120A_FAST_CHG); +} + +uint8_t BQ25120A::getLDOControlRegister() +{ + return readByte(BQ25120A_ADDRESS, BQ25120A_LDO_CTRL); +} + +bool BQ25120A::runsOnBattery(uint8_t address){ + uint8_t faults = readByteUnprotected(address, BQ25120A_FAULTS); + // Read VIN under voltage fault (VIN_UV on Bit 6) from the faults register. + bool runsOnBattery = (faults & 0b01000000) != 0; + return runsOnBattery; +} + +void BQ25120A::writeByte(uint8_t address, uint8_t subAddress, uint8_t data) +{ + nicla::_i2c_mutex.lock(); + // Only enter active mode when runnning on battery. + // When powered from VIN, driving CD HIGH would disable charging. + if(runsOnBattery(address)){ + setHighImpedanceModeEnabled(false); + } + Wire1.beginTransmission(address); + Wire1.write(subAddress); + Wire1.write(data); + Wire1.endTransmission(); + nicla::_i2c_mutex.unlock(); + setHighImpedanceModeEnabled(true); +} + +uint8_t BQ25120A::readByteUnprotected(uint8_t address, uint8_t subAddress){ + Wire1.beginTransmission(address); + Wire1.write(subAddress); + Wire1.endTransmission(false); + Wire1.requestFrom(address, 1); + uint32_t timeout = 100; + uint32_t start_time = millis(); + while(!Wire1.available() && (millis() - start_time) < timeout) {} + return Wire1.read(); +} + +uint8_t BQ25120A::readByte(uint8_t address, uint8_t subAddress) +{ + nicla::_i2c_mutex.lock(); + // Only enter active mode when runnning on battery. + // When powered from VIN, driving CD HIGH would disable charging. + if(runsOnBattery(address)){ + setHighImpedanceModeEnabled(false); + } + uint8_t ret = readByteUnprotected(address, subAddress); + nicla::_i2c_mutex.unlock(); + setHighImpedanceModeEnabled(true); + return ret; +} + +void BQ25120A::setHighImpedanceModeEnabled(bool enabled) { + if(enabled){ + cd = 0; + } else { + cd = 1; + delayMicroseconds(128); // Give some time to the BQ25120A to wake up + } +} \ No newline at end of file diff --git a/libraries/Nicla_System/src/BQ25120A.h b/libraries/Nicla_System/src/BQ25120A.h index 7c3317382..8cfedf654 100644 --- a/libraries/Nicla_System/src/BQ25120A.h +++ b/libraries/Nicla_System/src/BQ25120A.h @@ -4,13 +4,13 @@ #define BQ25120A_ADDRESS 0x6A // Register Map -// https://www.ti.com/lit/ds/symlink/bq25120a.pdf?ts=1610608851953&ref_url=https%253A%252F%252Fwww.startpage.com%252F +// https://www.ti.com/lit/ds/symlink/bq25120a.pdf #define BQ25120A_STATUS 0x00 #define BQ25120A_FAULTS 0x01 #define BQ25120A_TS_CONTROL 0x02 #define BQ25120A_FAST_CHG 0x03 #define BQ25120A_TERMINATION_CURR 0x04 -#define BQ25120A_BATTERY_CTRL 0x05 +#define BQ25120A_BATTERY_CTRL 0x05 // Battery Voltage Control Register #define BQ25120A_SYS_VOUT_CTRL 0x06 #define BQ25120A_LDO_CTRL 0x07 #define BQ25120A_PUSH_BUTT_CTRL 0x08 @@ -25,10 +25,87 @@ class BQ25120A public: BQ25120A() {}; - uint8_t getStatus(); + /** + * @brief Gets the data from the status register. + * @see Section 9.6.1 of the datasheet. + * + * @return uint8_t The data from the status register. + */ + uint8_t getStatusRegister(); + + /** + * @brief Gets the data from the faults register. + * @see Section 9.6.2 of the datasheet. + * + * @return uint8_t The data from the faults register. + */ + uint8_t getFaultsRegister(); + + /** + * @brief Gets the data from the SYS VOUT Control Register. + * @see Section 9.6.7 of the datasheet. + * + * @return uint8_t The data from the SYS VOUT Control Register. + */ + uint8_t getLDOControlRegister(); + + /** + * @brief Gets the data from the fast charge control register. + * @see Section 9.6.4 of the datasheet. + * + * @return uint8_t The data from the fast charge control register. + */ + uint8_t getFastChargeControlRegister(); + + + /** + * @brief Determines if the board is charged from the battery. + * + * @return true If the board is powered from the battery. False, when powered from USB / VIN. + */ + bool runsOnBattery(uint8_t address); + + /** + * @brief Writes a byte to the BQ25120A over I2C. + * @param address The I2C address of the BQ25120A. + * @param subAddress The memory location of the register to write to. + * @param data The data to write to the register. + */ void writeByte(uint8_t address, uint8_t subAddress, uint8_t data); + + /** + * @brief Reads a byte from the BQ25120A over I2C. + * + * @param address The I2C address of the BQ25120A. + * @param subAddress The memory location of the register to read from. + * @return uint8_t The data read from the register. + */ uint8_t readByte(uint8_t address, uint8_t subAddress); + private: + /** + * @brief Set the High Impedance Mode Enabled or Disabled. + * When enabled, drives the CD pin low to enter high impedance mode. + * Note that this only applies when powered with a battery and the condition VIN < VUVLO is met. + * When VIN > VUVLO this enables charging instead. + * + * When disabled, drives the CD pin high to exit high impedance mode (Active Battery). + * When VIN > VUVLO this disables charging. + * When exiting this mode, charging resumes if VIN is present, CD is low and charging is enabled. + * + * @note The CD pin is internally pulled down. + * @param enabled Defines if the high impedance mode should be enabled or disabled. + */ + void setHighImpedanceModeEnabled(bool enabled); + + /** + * @brief Reads a byte from the BQ25120A over I2C without locking the bus through the mutex. + * + * @param address The I2C address of the BQ25120A. + * @param subAddress The memory location of the register to read from. + * @return uint8_t The data read from the register. + */ + uint8_t readByteUnprotected(uint8_t address, uint8_t subAddress); }; #endif diff --git a/libraries/Nicla_System/src/Nicla_System.cpp b/libraries/Nicla_System/src/Nicla_System.cpp index d01ab1d60..e9fc4cb41 100644 --- a/libraries/Nicla_System/src/Nicla_System.cpp +++ b/libraries/Nicla_System/src/Nicla_System.cpp @@ -11,31 +11,42 @@ RGBled nicla::leds; BQ25120A nicla::_pmic; -rtos::Mutex nicla::i2c_mutex; +rtos::Mutex nicla::_i2c_mutex; bool nicla::started = false; -uint8_t nicla::_chg_reg = 0; +uint8_t nicla::_fastChargeRegisterData = 0; -void nicla::pingI2CThd() { - while(1) { - // already protected by a mutex on Wire operations - checkChgReg(); - delay(10000); +/// Enabled is the default value also represented in the TS Control Register (Bit 7 = 1). +bool nicla::_ntcEnabled = true; + +void nicla::pingI2C(bool useWriteOperation) { + // PMIC commands already protected by a mutex on Wire operations. + if(useWriteOperation){ + // Write the current charging settings to the register to reset the watchdog timer. + _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_FAST_CHG, _fastChargeRegisterData); + } else { + _pmic.getStatusRegister(); } } -bool nicla::begin(bool mounted_on_mkr) +bool nicla::begin(bool mountedOnMkr) { - if (mounted_on_mkr) { + if (mountedOnMkr) { // GPIO3 is on MKR RESET pin, so we must configure it HIGH or it will, well, reset the board :) - pinMode(p25, OUTPUT); pinMode(P0_10, OUTPUT); digitalWrite(P0_10, HIGH); } Wire1.begin(); - _chg_reg = _pmic.readByte(BQ25120A_ADDRESS, BQ25120A_FAST_CHG); + _fastChargeRegisterData = _pmic.getFastChargeControlRegister(); + #ifndef NO_NEED_FOR_WATCHDOG_THREAD + // If not using the BHY2 library, we need to start a thread to ping the PMIC every 10 seconds. static rtos::Thread th(osPriorityHigh, 768, nullptr, "ping_thread"); - th.start(&nicla::pingI2CThd); + th.start([]() { + while(1) { + pingI2C(); + delay(10000); + } + }); #endif started = true; @@ -64,7 +75,7 @@ bool nicla::enable3V3LDO() { uint8_t ldo_reg = 0xE4; _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_LDO_CTRL, ldo_reg); - if (_pmic.readByte(BQ25120A_ADDRESS, BQ25120A_LDO_CTRL) != ldo_reg) { + if (_pmic.getLDOControlRegister() != ldo_reg) { return false; } return true; @@ -74,7 +85,7 @@ bool nicla::enable1V8LDO() { uint8_t ldo_reg = 0xA8; _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_LDO_CTRL, ldo_reg); - if (_pmic.readByte(BQ25120A_ADDRESS, BQ25120A_LDO_CTRL) != ldo_reg) { + if (_pmic.getLDOControlRegister() != ldo_reg) { return false; } return true; @@ -82,13 +93,10 @@ bool nicla::enable1V8LDO() bool nicla::disableLDO() { - uint8_t ldo_reg = _pmic.readByte(BQ25120A_ADDRESS, BQ25120A_LDO_CTRL); - ldo_reg &= 0x7F; + uint8_t ldo_reg = _pmic.getLDOControlRegister(); + ldo_reg &= 0x7F; // Zeroes the EN_LS_LDO bit to turn it off _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_LDO_CTRL, ldo_reg); - if (_pmic.readByte(BQ25120A_ADDRESS, BQ25120A_LDO_CTRL) != ldo_reg) { - return false; - } - return true; + return _pmic.getLDOControlRegister() == ldo_reg; } bool nicla::enterShipMode() @@ -97,90 +105,317 @@ bool nicla::enterShipMode() // | B7 | B6 | B5 | B4 | B3 | B2 | B1 | B0 | // | RO | RO | EN_SHIPMODE | RO | RO | RO | RO | RO | - uint8_t status_reg = _pmic.getStatus(); + uint8_t status_reg = _pmic.getStatusRegister(); status_reg |= 0x20; _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_STATUS, status_reg); } -uint8_t nicla::readLDOreg() +bool nicla::enableCharging(uint16_t mA) { - return _pmic.readByte(BQ25120A_ADDRESS, BQ25120A_LDO_CTRL); -} + /* + The ICHRG is calculated using the following equation: + - If ICHRG_RANGE (Bit 7) is 0, then ICHRG = 5 mA + ICHRGCODE x 1 mA. + - If ICHRG_RANGE (Bit 7) is 1, then ICHRG = 40 mA + ICHRGCODE x 10 mA. + - If a value greater than 35 mA (ICHRG_RANGE = 0) or 300 mA (ICHRG_RANGE = 1) is written, + the setting goes to 35 mA or 300 mA respectively, except if the ICHRG bits are all 1 (that is, 11111), + then the externally programmed value is used. See section 9.6.4 in the datasheet. + */ -bool nicla::ntc_disabled; + if (mA > 300) { + mA = 300; + } -bool nicla::enableCharge(uint8_t mA, bool disable_ntc) -{ if (mA < 5) { - _chg_reg = 0x3; - } else if (mA < 35) { - _chg_reg = ((mA-5) << 2); + mA = 5; + } + + if(mA > 35 && mA < 40) { + mA = 35; + } + + if (mA <= 35) { + // Values 5 mA to 35 mA + _fastChargeRegisterData = ((mA-5) << 2); // e.g. 20mA - 5mA = 15mA << 2 -> 0b00111100 } else { - _chg_reg = (((mA-40)/10) << 2) | 0x80; + // Values 40 mA to 300 mA + // e.g. (200mA - 40mA) / 10 = 16mA << 2 -> 0b01000000 | 0x80 -> 0b11000000 + _fastChargeRegisterData = (((mA-40)/10) << 2) | 0x80; } - _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_FAST_CHG, _chg_reg); - // For very depleted batteries, set ULVO at the very minimum to re-enable charging + _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_FAST_CHG, _fastChargeRegisterData); + + // For very depleted batteries, set BULVO to the very minimum to re-enable charging. + // 2.2V or 2.0V are the minimum values for BULVO. The latter is not mentioned in the datasheet + // but it looks like a typo since 2.2V is mentioned twice. See: Table 22 in the datasheet. + // Also sets the input current limit to 350mA. _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_ILIM_UVLO_CTRL, 0x3F); - // Disable TS and interrupt on charge - ntc_disabled = disable_ntc; - if (ntc_disabled) { - _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_TS_CONTROL, 1 << 3); + return _pmic.getFastChargeControlRegister() == _fastChargeRegisterData; +} + +bool nicla::configureChargingSafetyTimer(ChargingSafetyTimerOption option){ + // See: Table 24 in the datasheet. + // The two bits need to be shifted to skip the unused LSB. + uint8_t timerValue = static_cast(option) << 1; + uint8_t dpmTimerRegisterData = _pmic.readByte(BQ25120A_ADDRESS, BQ25120A_VIN_DPM); + + dpmTimerRegisterData &= 0b11111001; // Clear bits 1 and 2 + dpmTimerRegisterData |= timerValue; // Update bits 1 and 2 + _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_VIN_DPM, dpmTimerRegisterData); + + return _pmic.readByte(BQ25120A_ADDRESS, BQ25120A_VIN_DPM) == dpmTimerRegisterData; +} + +bool nicla::disableCharging() +{ + // Set Bit 1 to 1 to disable charging. + _fastChargeRegisterData |= 0b10; + _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_FAST_CHG, _fastChargeRegisterData); + return _pmic.getFastChargeControlRegister() == _fastChargeRegisterData; +} + +bool nicla::runsOnBattery() { + return _pmic.runsOnBattery(BQ25120A_ADDRESS); +} + +uint8_t nicla::getBatteryFaults() { + // Skips the mask bits (4 LSBs) + return (_pmic.getFaultsRegister() >> 4) & 0b1111; +} + +void nicla::setBatteryNTCEnabled(bool enabled){ + if (_ntcEnabled != enabled) { + _ntcEnabled = enabled; + + // Read the current TS_CONTROL register value + uint8_t tsControlRegister = _pmic.readByte(BQ25120A_ADDRESS, BQ25120A_TS_CONTROL); + + if (_ntcEnabled) { + // Set bit 7 and bit 3 to 1 to enable temperature sense and interrupt on charge status change. + tsControlRegister |= 0b10001000; + } else { + // Set bit 7 and bit 3 to 0 to disable temperature sense and interrupt on charge status change. + // INT only shows faults and does not show charge status. + tsControlRegister &= 0b01110111; + } + + _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_TS_CONTROL, tsControlRegister); } +} + +float nicla::getRegulatedBatteryVoltage(){ + /* + According to https://www.ti.com/lit/ds/symlink/bq25120a.pdf Page 40: + + +---------+--------------------+ + | Bit | Regulation Voltage | + +---------+--------------------+ + | 7 (MSB) | 640 mV | + | 6 | 320 mV | + | 5 | 160 mV | + | 4 | 80 mV | + | 3 | 40 mV | + | 2 | 20 mV | + | 1 | 10 mV | + | 0 (LSB) | – | + +---------+--------------------+ + + // Example: 01111000 results in + // 3.6 + 0.32 + 0.16 + 0.08 + 0.04 = 4.2V which is the default value after reset + */ - // also set max battery voltage to 4.2V (VBREG) - // _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_BATTERY_CTRL, (4.2f - 3.6f)*100); + // Read the Battery Voltage Control Register that holds the regulated battery voltage + uint8_t data = _pmic.readByte(BQ25120A_ADDRESS, BQ25120A_BATTERY_CTRL); + int milliVolts = 3600; // 3.6V is the minimum voltage + + // Shift the data to the right by 1 bit to remove the LSB that is not used. + uint8_t shiftedData = (data >> 1) & 0b01111111; + milliVolts += shiftedData * 10; + + return milliVolts / 1000.0f; - return _pmic.readByte(BQ25120A_ADDRESS, BQ25120A_FAST_CHG) == _chg_reg; } -uint16_t nicla::getFault() { - uint16_t tmp = _pmic.readByte(BQ25120A_ADDRESS, BQ25120A_FAULTS) << 8; - tmp |= (_pmic.readByte(BQ25120A_ADDRESS, BQ25120A_TS_CONTROL) & 0x60); - return tmp; +void nicla::setRegulatedBatteryVoltage(float voltage){ + if (voltage < 3.6f){ + voltage = 3.6f; + } else if (voltage > 4.2f) { + voltage = 4.2f; + } + + // The formula is: VBATREG = 3.6 V + Sum(VBREG[Bit 7:1]) + + /* + +---------+--------------------+ + | Bit | Regulation Voltage | + +---------+--------------------+ + | 7 (MSB) | 640 mV | + | 6 | 320 mV | + | 5 | 160 mV | + | 4 | 80 mV | + | 3 | 40 mV | + | 2 | 20 mV | + | 1 | 10 mV | + | 0 (LSB) | – | + +---------+--------------------+ + */ + + uint16_t voltageAddition = (voltage - 3.6f) * 100; + // Shift one bit to the left because the LSB is not used. + uint8_t value = voltageAddition << 1; + // e.g. 4.2V - 3.6V = 0.6V * 100 = 60. 60 << 1 = 120 = 01111000 + + _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_BATTERY_CTRL, value); } -int nicla::getBatteryStatus() { - _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_BATT_MON, 1); - delay(3); - uint8_t data = _pmic.readByte(BQ25120A_ADDRESS, BQ25120A_BATT_MON); - float percent = 0.6f + (data >> 5) * 0.1f + ((data >> 2) & 0x7) * 0.02f; - - int res = 0; - if (percent >= 0.98) { - res |= BATTERY_FULL; - } else if (percent >= 0.94){ - res |= BATTERY_ALMOST_FULL; - } else if (percent >= 0.90){ - res |= BATTERY_HALF; - } else if (percent >= 0.86){ - res |= BATTERY_ALMOST_EMPTY; - } else { - res |= BATTERY_EMPTY; +float nicla::getCurrentBatteryVoltage(){ + auto percentage = getBatteryVoltagePercentage(); + if (percentage < 0) { + return 0; } + return getRegulatedBatteryVoltage() / 100 * percentage; +} + +int8_t nicla::getBatteryVoltagePercentage(bool useLatchedValue) { + /* + * 9.3.4 Voltage Based Battery Monitor (Page 20) + * The device implements a simple voltage battery monitor which can be used to determine the depth of discharge. + * Prior to entering High-Z mode, the device will initiate a VBMON reading. The host can read the latched value for + * the no-load battery voltage, or initiate a reading using VBMON_READ to see the battery voltage under a known + * load. The register will be updated and can be read 2ms after a read is initiated. The VBMON voltage threshold is + * readable with 2% increments with ±1.5% accuracy between 60% and 100% of VBATREG using the VBMON_TH + * registers. Reading the value during charge is possible, but for the most accurate battery voltage indication, it is + * recommended to disable charge, initiate a read, and then re-enable charge. + */ + /* + According to https://www.ti.com/lit/ds/symlink/bq25120a.pdf Page 45: + MSB = Bit 7, LSB = Bit 0 - if (!ntc_disabled) { - auto ts = ((_pmic.readByte(BQ25120A_ADDRESS, BQ25120A_TS_CONTROL) >> 5) & 0x3); - if (ts == 1) { - res |= BATTERY_COLD; - } else if (ts == 2) { - res |= BATTERY_COOL; - } else if (ts == 3) { - res |= BATTERY_HOT; + +----------+------------------------+ + | Bits 5+6 | Meaning | + +----------+------------------------+ + | 11 | 90% to 100% of VBATREG | + | 10 | 80% to 90% of VBATREG | + | 01 | 70% to 80% of VBATREG | + | 00 | 60% to 70% of VBATREG | + +----------+------------------------+ + + +----------+-------------------------+ + | Bits 2-4 | Meaning | + +----------+-------------------------+ + | 111 | Above 8% of VBMON_RANGE | + | 110 | Above 6% of VBMON_RANGE | + | 011 | Above 4% of VBMON_RANGE | + | 010 | Above 2% of VBMON_RANGE | + | 001 | Above 0% of VBMON_RANGE | + +----------+-------------------------+ + + Example: 0 11 111 00 -> 90% + 8% = 98 - 100% of VBATREG + */ + constexpr uint8_t BAT_UVLO_FAULT = 0b00100000; // Battery Under-Voltage Lock-Out fault + uint8_t faults = _pmic.getFaultsRegister(); + if(faults & BAT_UVLO_FAULT) return -1; // Battery is not connected or voltage is too low + + + if(!useLatchedValue){ + // Disable charging while reading battery percentage. SEE chapter 9.3.4 + bool chargingEnabled = (_fastChargeRegisterData & 0b10) == 0; // Bit 1 is 0 if charging is enabled. + + if(chargingEnabled) { + disableCharging(); + } + // Write 1 to bit 7 VBMON_READ to trigger a new reading + _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_BATT_MON, 0b10000000); + + delay(3); // According to datasheet, 2ms is enough, but we add 1ms for safety + + if(chargingEnabled) { + // Re-enable charging by setting bit 1 to 0 + _fastChargeRegisterData &= 0b11111101; + _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_FAST_CHG, _fastChargeRegisterData); } } + uint8_t data = _pmic.readByte(BQ25120A_ADDRESS, BQ25120A_BATT_MON); + + // If bit 2 - 7 are all zeroes, the battery status could not be read + if((data & 0b11111100) == 0) return -2; + + // Extract bits 5 and 6 + // Masking is optional because the MSB is always 0 + uint8_t bits56 = (data >> 5) & 0b11; + + // Extract bits 2 to 4 + uint8_t bits234 = (data >> 2) & 0b111; + + // FIXME: The pattern 000 is not defined in the datasheet but still occurs + // along with a valid bits56 pattern. We assume that it means 0%. + // if(bits234 == 0b000) return -1; // Battery status could not be read + + // Lookup tables for mapping bit patterns to percentage values + // The datasheet says that the threshold values are above what's written in the table. + // Therefore we use the next higher value in steps of 2%. + // That way the final percentage is always rounded up and can reach 100%. + int thresholdLookup[] = {0, 2, 4, 6, 0, 0, 8, 10}; - return res; + // bits56 has a range of 0 to 3, so we multiply it by 10 and add 60 to get a range of 60 to 90 + int percentageTens = 60 + (bits56 * 10); + // Map bit patterns to percentage values using lookup table + int percentageOnes = thresholdLookup[bits234]; + + // Calculate the final percentage + return percentageTens + percentageOnes; } -void nicla::checkChgReg() -{ - if (_chg_reg != _pmic.readByte(BQ25120A_ADDRESS, BQ25120A_FAST_CHG)) { - _pmic.writeByte(BQ25120A_ADDRESS, BQ25120A_FAST_CHG, _chg_reg); +BatteryChargeLevel nicla::getBatteryChargeLevel() { + auto percent = getBatteryVoltagePercentage(); + + if (percent >= 98) { + return BatteryChargeLevel::Full; + } else if (percent >= 94){ + return BatteryChargeLevel::AlmostFull; + } else if (percent >= 90){ + return BatteryChargeLevel::HalfFull; + } else if (percent >= 86){ + return BatteryChargeLevel::AlmostEmpty; + } else if(percent < 86 && percent > 0) { + // < 84% is considered empty + return BatteryChargeLevel::Empty; + } else { + // Battery status could not be read + return BatteryChargeLevel::Unknown; } } +BatteryTemperature nicla::getBatteryTemperature() { + if(!_ntcEnabled) return BatteryTemperature::Normal; + + /* + +------+-------------------------------------------------------------+ + | Bits | Description | + +------+-------------------------------------------------------------+ + | 00 | Normal, No TS fault | + | 01 | TS temp < TCOLD or TS temp > THOT (Charging suspended) | + | 10 | TCOOL > TS temp > TCOLD (Charging current reduced by half) | + | 11 | TWARM < TS temp < THOT (Charging voltage reduced by 140 mV) | + +------+-------------------------------------------------------------+ + */ + // Extract bits 5 and 6 (TS_FAULT0 and TS_FAULT1) + uint8_t temperatureSenseFault = _pmic.readByte(BQ25120A_ADDRESS, BQ25120A_TS_CONTROL) >> 5 & 0b11; + return static_cast(temperatureSenseFault); +} + + +OperatingStatus nicla::getOperatingStatus() { + // Extract bits 6 and 7 + uint8_t status = _pmic.getStatusRegister() >> 6 & 0b11; + return static_cast(status); +} + +void nicla::checkChgReg(){ + pingI2C(); +} + + I2CLed LEDR(red); I2CLed LEDG(green); I2CLed LEDB(blue); diff --git a/libraries/Nicla_System/src/Nicla_System.h b/libraries/Nicla_System/src/Nicla_System.h index 85849ba08..176851cd1 100644 --- a/libraries/Nicla_System/src/Nicla_System.h +++ b/libraries/Nicla_System/src/Nicla_System.h @@ -8,46 +8,258 @@ #include #include +// Deprecated. Whether or not to use a write operation by default +// can now be controlled with the default paramter of pingI2C(). #define USE_FASTCHG_TO_KICK_WATCHDOG 1 -#define BATTERY_FULL 5 -#define BATTERY_ALMOST_FULL 4 -#define BATTERY_HALF 3 -#define BATTERY_ALMOST_EMPTY 2 -#define BATTERY_EMPTY 1 -#define BATTERY_COLD (1 << 4) -#define BATTERY_COOL (2 << 4) -#define BATTERY_HOT (3 << 4) -#define BATTERY_CHARGING (1 << 7) +enum class OperatingStatus { + Ready = 0b00, + Charging = 0b01, + ChargingComplete = 0b10, + Error = 0b11 +}; + +// 3 bits are used to indicate the battery charge level +enum class BatteryChargeLevel { + Unknown = 0b000, + Empty = 0b001, + AlmostEmpty = 0b010, + HalfFull = 0b011, + AlmostFull = 0b100, + Full = 0b101 +}; + +// 2 bits are used to indicate the battery temperature +enum class BatteryTemperature { + Normal = 0b00, + Extreme = 0b01, + Cool = 0b10, + Warm = 0b11 +}; + +enum class ChargingSafetyTimerOption { + ThirtyMinutes = 0b00, + ThreeHours = 0b01, + NineHours = 0b10, + Disabled = 0b11 +}; + class nicla { public: - static bool begin(bool mounted_on_mkr = false); + + /** + * @brief Initializes the Nicla Sense ME board. + * + * @param mountedOnMkr Defines whether the board is mounted as a shield on a MKR board or not. + * This is used to drive the pin high that connects to the reset pin + * of the MKR board to prevent it from resetting. + * @return true if the board is initialized successfully. + */ + static bool begin(bool mountedOnMkr = false); + + /** + * @brief Enables the 3.3V LDO voltage regulator. + * + * @return true if the LDO is enabled successfully. + * This is done by verifying that the register was written correctly. + */ static bool enable3V3LDO(); + + /** + * @brief Enables the 1.8V LDO voltage regulator. + * + * @return true if the LDO is enabled successfully. + * This is done by verifying that the register was written correctly. + */ static bool enable1V8LDO(); + + /** + * @brief Disables the LDO voltage regulator. + * + * @return true if the LDO is disabled successfully. + * This is done by verifying that the register was written correctly. + */ static bool disableLDO(); + + /** + * @brief Enter Ship Mode. This is used during the shipping time or the shelf time of the product. + * This will turn off the battery FET and thus isolate the battery from the system (turns off leakage path). + * So, whatever is leaky in your system won't be leaking out of the battery during this time. + * + * @return true if the ship mode is entered successfully. + */ static bool enterShipMode(); - static uint8_t readLDOreg(); - static bool enableCharge(uint8_t mA = 20, bool disable_ntc = true); - static int getBatteryStatus(); - static uint16_t getFault(); - static bool ntc_disabled; + /** + * @brief Enables fast charging of the battery. By default charging is already enabled when the board is powered. + * The default charging current without enabling fast charging is 10mA. Charging can be disabled by calling disableCharging(). + * + * @param mA The desired milliampere (mA) charging current. Range: 5mA - 35mA and 40mA - 300mA. The default value is 20mA. + * A safe default charging current value that works for most common LiPo batteries is 0.5C, which means charging at a rate equal to half of the battery's capacity. + * For example, a 200mAh battery could be charged at 100mA (0.1A). + * This charging rate is generally safe for most LiPo batteries and provides a good balance between charging speed and battery longevity. + * @note If your battery doesn't have an NTC thermistor, the charging speed will be limited to ~16mA. + * @note There is a saftey timer that will stop the charging after 3 hours by default. + * This can be configured by calling configureChargingSafetyTimer(). + * @return true If the fast charging is enabled successfully. False, otherwise. + * @see disableCharging() + */ + static bool enableCharging(uint16_t mA = 20); + + /** + * @brief Configures the charging safety timer after which the charging is stopped. + * This is useful to prevent overcharging the battery. The timer can have one of the following options: + * 30 minutes, 3 hours, 9 hours or disabled. + * + * @param option One of the following options: ThirtyMinutes, ThreeHours, NineHours or Disabled. + * @return true if the charging safety timer is configured successfully, false otherwise. + */ + static bool configureChargingSafetyTimer(ChargingSafetyTimerOption option); + + /** + * @brief Disables charging of the battery. It can be resumed by calling enableCharging(). + * + * @return true If the charging is disabled successfully. False, otherwise. + */ + static bool disableCharging(); + + /** + * @brief Determines if the board is charged from the battery. + * + * @return true If the board is powered from the battery. False, when powered from USB / VIN. + */ + static bool runsOnBattery(); + + /** + * @brief Enables or disables the negative temperature coefficient (NTC) thermistor. It is enabled by default. + * NTCs are used to prevent the batteries from being charged at temperatures that are too high or too low. + * Set to disabled for standard LiPo batteries without NTC. + * If your battery has only a plus and minus wire, it does not have an NTC. + * @note Disabling the NTC will also disable the on-charge-state-change interrupt. + * @param enabled Whether to enabled the NTC. + */ + static void setBatteryNTCEnabled(bool enabled); + + /** + * @brief Get the Regulated Battery Voltage in Volts. + * + * @return float The regulated battery voltage in Volts. The default regulated voltage is 4.2V. + */ + static float getRegulatedBatteryVoltage(); + + /** + * @brief Set the Regulated Battery Voltage. + * + * @param voltage The voltage in the range of 3.6V to 4.65V. + */ + static void setRegulatedBatteryVoltage(float voltage); + + /** + * @brief Get the Current Battery Voltage in Volts. This value is calculated by multiplying + * the regulated voltage by the battery percentage. + * + * @return float The current battery voltage in Volts. + */ + static float getCurrentBatteryVoltage(); + + /** + * @brief Get the percentage of the battery's regulated voltage under a known load. + * + * The accuracy of the battery voltage monitor (VBAT monitor) is between -3.5% and +3.5% of the regulated voltage (VBATREG). + * @note This does not denote the actual battery charge level but the percentage of the fully charged voltage. + * Many common LiPo batteries have a nominal voltage of 3.7V and a fully charged voltage of 4.2V. + * For a 4.2V regulated voltage battery < 84% is considered empty. < 60% is considered critical; the battery may be damaged. + * @param useLatchedValue Before entering a low power state (High Impedance mode), the device will determine the voltage + * and store it in a latched register. If true, the latched (stored) value is returned. + * If false, a new reading is taken from the PMIC. The default is false, so a new reading is taken. + * @return int8_t The percentage of the regulated voltage in the range of 60% to 100%. + * A value of < 0 indicates that the battery percentage could not be determined. + */ + static int8_t getBatteryVoltagePercentage(bool useLatchedValue = false); + + /** + * @brief Get the Battery Charge level encoded as a number (0-5). The following values are possible: + * "Unknown", "Empty", "Almost Empty", "Half Full", "Almost Full", "Full" + * + * @return BatteryChargeLevel The battery charge level represented by one of the following values: + * BatteryChargeLevel::Unknown, BatteryChargeLevel::Empty, BatteryChargeLevel::AlmostEmpty, + * BatteryChargeLevel::HalfFull, BatteryChargeLevel::AlmostFull, BatteryChargeLevel::Full + */ + static BatteryChargeLevel getBatteryChargeLevel(); + + /** + * @brief Get the Battery Temperature using the negative temperature coefficient (NTC) thermistor. + * The following values are possible: "Normal", "Extreme", "Cool", "Warm". + * When the battery is cool, the charging current is reduced by half. + * When the battery is warm, the charging current is reduced by 140 mV. + * When the battery is unter an extreme temperature (hot or cold), the charging is suspended. + * @note If the battery isn't configured to have a NTC, the temperature is reported as "Normal". + * The presence of the NTC is not determined automatically and needs to be set using the setBatteryNTCEnabled() function. + * @see setBatteryNTCEnabled() + * @return BatteryTemperature The battery temperature represented by one of the following constants: + * BatteryTemperature::Normal, BatteryTemperature::Extreme, BatteryTemperature::Cool, BatteryTemperature::Warm + */ + static BatteryTemperature getBatteryTemperature(); + + /** + * @brief Returns potential battery faults retrieved from the fault register. + * + * - Bit 3: 1 - VIN overvoltage fault. VIN_OV continues to show fault after an I2C read as long as OV exists + * - Bit 2: 1 - VIN undervoltage fault. VIN_UV is set when the input falls below VSLP. VIN_UV fault shows only one time. Once read, VIN_UV clears until the the UVLO event occurs. + * - Bit 1: 1 – BAT_UVLO fault. BAT_UVLO continues to show fault after an I2C read as long as BAT_UVLO conditions exist. + * - Bit 0: 1 – BAT_OCP fault. BAT_OCP is cleared after I2C read. + * + * @note Some of the registers are not persistent. See chapter 9.6.2 and 9.6.3 of the datasheet. + * @return uint8_t The battery faults encoded in a 16bit integer. + */ + static uint8_t getBatteryFaults(); + + + /** + * @brief Get the current operating status of the PMIC. + * @note If it doesn't report 'Charging' even though you enabled charging with enableCharging(), the battery might be full. + * @return OperatingStatus One of the following: Ready, Charging, ChargingComplete, Error. + */ + static OperatingStatus getOperatingStatus(); + + /// Provides access to the IS31FL3194 LED driver that drives the RGB LED. static RGBled leds; static BQ25120A _pmic; + + /// Flag to check if the begin function has been called. This is used to automatically call the begin function if necessary. + static bool started; friend class RGBled; friend class BQ25120A; friend class Arduino_BHY2; - static bool started; private: - static void pingI2CThd(); + /// Defines if the connected battery has a negative temperature coefficient (NTC) thermistor. + static bool _ntcEnabled; + + /** + * @brief Pings the I2C interface by querying the PMIC's fast charge register every 10 seconds. + * This is invoked by a thread and is meant to kick the watchdog timer to prevent the PMIC from entering a low power state. + * The I2C interface reset timer for the host is 50 seconds. + * @param useWriteOperation If true, a write operation to a register is performed to reset the watchdog timer. + * If false, a read operation is performed. The default is false. + */ + static void pingI2C(bool useWriteOperation = false); + + [[deprecated("Use pingI2C() instead.")]] static void checkChgReg(); - static rtos::Mutex i2c_mutex; - static uint8_t _chg_reg; + + /** + * A cached version of the fast charge settings for the PMIC. + * This is used to avoid unnecessary I2C communication. + **/ + static uint8_t _fastChargeRegisterData; + + /// Mutex to prevent concurrent access to the I2C interface. + static rtos::Mutex _i2c_mutex; }; #endif \ No newline at end of file diff --git a/libraries/Nicla_System/src/RGBled.cpp b/libraries/Nicla_System/src/RGBled.cpp index 11486c7b3..395e1b79f 100644 --- a/libraries/Nicla_System/src/RGBled.cpp +++ b/libraries/Nicla_System/src/RGBled.cpp @@ -150,17 +150,17 @@ void RGBled::ledBlink(RGBColors color, uint32_t duration) void RGBled::writeByte(uint8_t address, uint8_t subAddress, uint8_t data) { - nicla::i2c_mutex.lock(); + nicla::_i2c_mutex.lock(); Wire1.beginTransmission(address); Wire1.write(subAddress); Wire1.write(data); Wire1.endTransmission(); - nicla::i2c_mutex.unlock(); + nicla::_i2c_mutex.unlock(); } uint8_t RGBled::readByte(uint8_t address, uint8_t subAddress) { - nicla::i2c_mutex.lock(); + nicla::_i2c_mutex.lock(); //char response = 0xFF; Wire1.beginTransmission(address); Wire1.write(subAddress); @@ -170,6 +170,6 @@ uint8_t RGBled::readByte(uint8_t address, uint8_t subAddress) uint32_t start_time = millis(); while(!Wire1.available() && (millis() - start_time) < timeout) {} uint8_t ret = Wire1.read(); - nicla::i2c_mutex.unlock(); + nicla::_i2c_mutex.unlock(); return ret; } \ No newline at end of file diff --git a/libraries/Nicla_System/src/pmic_driver.cpp b/libraries/Nicla_System/src/pmic_driver.cpp deleted file mode 100644 index 097214468..000000000 --- a/libraries/Nicla_System/src/pmic_driver.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include -#include -#include "BQ25120A.h" -#include "Nicla_System.h" -#include "DigitalOut.h" - -static mbed::DigitalOut cd(p25); - -uint8_t BQ25120A::getStatus() -{ - uint8_t c = readByte(BQ25120A_ADDRESS, BQ25120A_STATUS); // Read PRODUCT_ID register for BQ25120A - return c; -} - -void BQ25120A::writeByte(uint8_t address, uint8_t subAddress, uint8_t data) -{ - cd = 1; - nicla::i2c_mutex.lock(); - Wire1.beginTransmission(address); - Wire1.write(subAddress); - Wire1.write(data); - Wire1.endTransmission(); - nicla::i2c_mutex.unlock(); - cd = 0; -} - -uint8_t BQ25120A::readByte(uint8_t address, uint8_t subAddress) -{ - cd = 1; - nicla::i2c_mutex.lock(); - Wire1.beginTransmission(address); - Wire1.write(subAddress); - Wire1.endTransmission(false); - Wire1.requestFrom(address, 1); - uint32_t timeout = 100; - uint32_t start_time = millis(); - while(!Wire1.available() && (millis() - start_time) < timeout) {} - uint8_t ret = Wire1.read(); - nicla::i2c_mutex.unlock(); - cd = 0; - return ret; -}