diff --git a/build/devices/esp32/targets/m5stack_core2/host/provider.js b/build/devices/esp32/targets/m5stack_core2/host/provider.js index 54cb6ab7d4..cb0c7cae1c 100644 --- a/build/devices/esp32/targets/m5stack_core2/host/provider.js +++ b/build/devices/esp32/targets/m5stack_core2/host/provider.js @@ -28,6 +28,29 @@ import Serial from "embedded:io/serial"; import SMBus from "embedded:io/smbus"; import SPI from "embedded:io/spi"; import PulseWidth from "embedded:io/pulsewidth"; +import BMI270 from "embedded:sensor/Accelerometer-Gyroscope-Magnetometer/BMI270"; +import MPU6886 from "embedded:sensor/Accelerometer-Gyroscope/MPU6886"; + +const ACCELERATION_SCALER = 1 / 9.80665; +const IMU_ADDRESS = 0x68; +const BMI270_CHIP_ID_ADDR = 0x00; +const BMI270_CHIP_ID = 0x24; +const MPU6886_WHO_AM_I_ADDR = 0x75; +const MPU6886_WHO_AM_I = 0x19; + +class Core2BMI270 extends BMI270 { + sample() { + const sample = super.sample(); + + if (sample.accelerometer) { + sample.accelerometer.x *= ACCELERATION_SCALER; + sample.accelerometer.y *= ACCELERATION_SCALER; + sample.accelerometer.z *= ACCELERATION_SCALER; + } + + return sample; + } +} const device = { I2C: { @@ -69,7 +92,50 @@ const device = { pin: { displayDC: 15, displaySelect: 5 + }, + sensor: { + IMU: class { + constructor(options) { + const sensor = { + ...device.I2C.internal, + address: IMU_ADDRESS, + io: device.io.SMBus + }; + + if (MPU6886_WHO_AM_I === readIMURegister(MPU6886_WHO_AM_I_ADDR)) + return new MPU6886({ + ...options, + sensor + }); + + if (BMI270_CHIP_ID === readIMURegister(BMI270_CHIP_ID_ADDR)) + return new Core2BMI270({ + ...options, + sensor + }); + + throw new Error("IMU not found"); + } + } } }; +function readIMURegister(register) { + let io; + try { + io = new device.io.SMBus({ + data: device.I2C.internal.data, + clock: device.I2C.internal.clock, + hz: 400_000, + address: IMU_ADDRESS + }); + return io.readUint8(register); + } + catch (e) { + } + finally { + io?.close(); + } +} + export default device; diff --git a/build/devices/esp32/targets/m5stack_core2/manifest.json b/build/devices/esp32/targets/m5stack_core2/manifest.json index 69ed12e49d..2293466448 100644 --- a/build/devices/esp32/targets/m5stack_core2/manifest.json +++ b/build/devices/esp32/targets/m5stack_core2/manifest.json @@ -1,13 +1,16 @@ { "build": { "UPLOAD_SPEED": "1500000", - "DEBUGGER_SPEED": "921600", + "DEBUGGER_SPEED": "460800", "SDKCONFIGPATH": "./sdkconfig", "PARTITIONS_FILE_FOR_TARGET": "./sdkconfig/partitions.csv" }, "include": [ + "$(MODDABLE)/modules/io/manifest.json", "$(MODDABLE)/modules/drivers/ili9341/manifest.json", "$(MODDABLE)/modules/drivers/ft6206/manifest.json", + "$(MODDABLE)/modules/drivers/sensors/bmi270/manifest.json", + "$(MODDABLE)/modules/drivers/sensors/mpu6886/manifest.json", "$(MODDABLE)/modules/drivers/axp192/manifest.json", "$(MODDABLE)/modules/drivers/axp2101/manifest.json" ], diff --git a/build/devices/esp32/targets/m5stack_core2/setup-target.js b/build/devices/esp32/targets/m5stack_core2/setup-target.js index 92db515666..13ec79dceb 100644 --- a/build/devices/esp32/targets/m5stack_core2/setup-target.js +++ b/build/devices/esp32/targets/m5stack_core2/setup-target.js @@ -20,6 +20,8 @@ import AXP192 from "axp192"; import AXP2101 from "axp2101"; +import BMI270 from "embedded:sensor/Accelerometer-Gyroscope-Magnetometer/BMI270"; +import EmbeddedSMBus from "embedded:io/smbus"; import MPU6886 from "mpu6886"; import AudioOut from "pins/audioout"; import Resource from "Resource"; @@ -33,10 +35,95 @@ const INTERNAL_I2C = Object.freeze({ scl: 22 }); +const INTERNAL_I2C_IO = Object.freeze({ + data: 21, + clock: 22 +}); +const ACCELERATION_SCALER = 1 / 9.80665; +const IMU_ADDRESS = 0x68; +const BMI270_CHIP_ID_ADDR = 0x00; +const BMI270_CHIP_ID = 0x24; +const MPU6886_WHO_AM_I_ADDR = 0x75; +const MPU6886_WHO_AM_I = 0x19; + const state = { handleRotation: nop, }; +class Core2BMI270 extends BMI270 { + #operation = "gyroscope"; + + constructor() { + super({ + sensor: { + ...INTERNAL_I2C_IO, + address: IMU_ADDRESS, + io: EmbeddedSMBus + } + }); + } + + configure(dictionary) { + if (dictionary?.operation) + this.#operation = dictionary.operation; + } + + sample() { + const sample = super.sample(); + let result; + + switch (this.#operation) { + case "accelerometer": + result = sample.accelerometer; + if (result) { + result.x *= ACCELERATION_SCALER; + result.y *= ACCELERATION_SCALER; + result.z *= ACCELERATION_SCALER; + } + return result; + case "gyroscope": + return sample.gyroscope; + case "temp": + return sample.thermometer?.temperature; + } + } +} + +function createAccelerometerGyro() { + if (MPU6886_WHO_AM_I === readIMURegister(MPU6886_WHO_AM_I_ADDR)) + return new MPU6886(INTERNAL_I2C); + + if (BMI270_CHIP_ID === readIMURegister(BMI270_CHIP_ID_ADDR)) + return new Core2BMI270; +} + +function getAccelerometerGyro() { + if (undefined === state.accelerometerGyro) + state.accelerometerGyro = createAccelerometerGyro(); + return state.accelerometerGyro; +} + +function readIMURegister(register) { + let io; + try { + io = new I2C({...INTERNAL_I2C, address: IMU_ADDRESS, throw: false}); + let result = io.write(register, false); + if (result instanceof Error) + return; + + result = io.read(1); + if (result instanceof Error) + return; + + return result?.[0]; + } + catch (e) { + } + finally { + io?.close(); + } +} + globalThis.Host = { Backlight: class { constructor(brightness = 100) { @@ -119,68 +206,68 @@ export default function (done) { } // accelerometer and gyrometer - const test = new I2C({...INTERNAL_I2C, address: 0x68, throw: false}); - test.write(0x75, false); - const ok = test.read(1); - test.close(); - if (undefined !== ok) { - state.accelerometerGyro = new MPU6886(INTERNAL_I2C); - - globalThis.accelerometer = { - onreading: nop, - }; + globalThis.accelerometer = { + onreading: nop, + }; - globalThis.gyro = { - onreading: nop, - }; + globalThis.gyro = { + onreading: nop, + }; - accelerometer.start = function (frequency) { - accelerometer.stop(); - state.accelerometerTimerID = Timer.repeat((id) => { - state.accelerometerGyro.configure({ - operation: "accelerometer", - }); - const sample = state.accelerometerGyro.sample(); - if (sample) { - state.handleRotation(sample); - accelerometer.onreading(sample); - } - }, frequency); - }; + accelerometer.start = function (frequency) { + accelerometer.stop(); + state.accelerometerTimerID = Timer.repeat((id) => { + const accelerometerGyro = getAccelerometerGyro(); + if (undefined === accelerometerGyro) + return; + + accelerometerGyro.configure({ + operation: "accelerometer", + }); + const sample = accelerometerGyro.sample(); + if (sample) { + state.handleRotation(sample); + accelerometer.onreading(sample); + } + }, frequency); + }; - gyro.start = function (frequency) { - gyro.stop(); - state.gyroTimerID = Timer.repeat((id) => { - state.accelerometerGyro.configure({ - operation: "gyroscope", + gyro.start = function (frequency) { + gyro.stop(); + state.gyroTimerID = Timer.repeat((id) => { + const accelerometerGyro = getAccelerometerGyro(); + if (undefined === accelerometerGyro) + return; + + accelerometerGyro.configure({ + operation: "gyroscope", + }); + const sample = accelerometerGyro.sample(); + if (sample) { + let { x, y, z } = sample; + const temp = x; + x = y * -1; + y = temp * -1; + z *= -1; + gyro.onreading({ + x, + y, + z, }); - const sample = state.accelerometerGyro.sample(); - if (sample) { - let { x, y, z } = sample; - const temp = x; - x = y * -1; - y = temp * -1; - z *= -1; - gyro.onreading({ - x, - y, - z, - }); - } - }, frequency); - }; + } + }, frequency); + }; - accelerometer.stop = function () { - if (undefined !== state.accelerometerTimerID) - Timer.clear(state.accelerometerTimerID); - delete state.accelerometerTimerID; - }; + accelerometer.stop = function () { + if (undefined !== state.accelerometerTimerID) + Timer.clear(state.accelerometerTimerID); + delete state.accelerometerTimerID; + }; - gyro.stop = function () { - if (undefined !== state.gyroTimerID) Timer.clear(state.gyroTimerID); - delete state.gyroTimerID; - }; - } + gyro.stop = function () { + if (undefined !== state.gyroTimerID) Timer.clear(state.gyroTimerID); + delete state.gyroTimerID; + }; // autorotate if (config.autorotate && globalThis.Application && globalThis.accelerometer) { diff --git a/build/devies/esp32/targets/m5atom_s3r/host/provider.js b/build/devies/esp32/targets/m5atom_s3r/host/provider.js new file mode 100644 index 0000000000..a7a99c87c5 --- /dev/null +++ b/build/devies/esp32/targets/m5atom_s3r/host/provider.js @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2025 Moddable Tech, Inc. + * + * This file is part of the Moddable SDK Runtime. + * + * The Moddable SDK Runtime is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Moddable SDK Runtime is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with the Moddable SDK Runtime. If not, see . + * + */ + +import Analog from "embedded:io/analog"; +import Digital from "embedded:io/digital"; +import DigitalBank from "embedded:io/digitalbank"; +import I2C from "embedded:io/i2c"; +import PulseCount from "embedded:io/pulsecount"; +import PWM from "embedded:io/pwm"; +import Serial from "embedded:io/serial"; +import SMBus from "embedded:io/smbus"; +import SPI from "embedded:io/spi"; +import Timer from "timer"; +import IMU from "embedded:sensor/Accelerometer-Gyroscope-Magnetometer/BMI270"; + +const ACCELERATION_SCALER = 1 / 9.80665; + +class Backlight { + #io; + + constructor(options) { + const io = this.#io = new SMBus({ + ...device.I2C.internal, + hz: 400_000, + address:48, + }); + + io.writeUint8(0x00, 0b01000000) + Timer.delay(1) + io.writeUint8(0x08, 0b00000001) + io.writeUint8(0x70, 0b00000000) + } + close() { + this.#io?.close(); + this.#io = undefined; + } + set brightness(value) { + if (value <= 0) value = 0; + else if (value >= 1) value = 255; + else value *= 255; + this.#io.writeUint8(0x0e, value) + } +} + +const device = { + I2C: { + default: { + io: I2C, + data: 2, + clock: 1, + }, + internal: { + io: I2C, + data: 45, + clock: 0, + }, + }, + SPI: { + default: { + io: SPI, + port: 3, + clock: 15, + out: 21, + }, + }, + Analog: { + default: { + io: Analog, + pin: 8, + }, + }, + io: { + Analog, + Digital, + DigitalBank, + I2C, + PulseCount, + PWM, + Serial, + SMBus, + SPI, + }, + pin: { + button: 41, + displaySelect: 14, + }, + peripheral: { + Backlight: class { + constructor() { + return new Backlight(); + } + }, + }, + sensor: { + IMU: class extends IMU { + constructor(options) { + super({ + ...options, + sensor: { + ...device.I2C.internal, + address: 0x68, + io: device.io.SMBus, + }, + }); + } + sample() { + const sample = super.sample(); + + if (sample.accelerometer) { + [sample.accelerometer.x, sample.accelerometer.y] = [-sample.accelerometer.y * ACCELERATION_SCALER, sample.accelerometer.x * ACCELERATION_SCALER]; + sample.accelerometer.z *= -ACCELERATION_SCALER; + } + if (sample.gyroscope) { + [sample.gyroscope.x, sample.gyroscope.y] = [-sample.gyroscope.y, sample.gyroscope.x]; + sample.gyroscope.z *= -1; + } + + return sample; + } + }, + }, +}; + +export default device; diff --git a/build/devies/esp32/targets/m5atom_s3r/manifest.json b/build/devies/esp32/targets/m5atom_s3r/manifest.json new file mode 100644 index 0000000000..51a1ac744b --- /dev/null +++ b/build/devies/esp32/targets/m5atom_s3r/manifest.json @@ -0,0 +1,94 @@ +{ + "build":{ + "ESP32_SUBCLASS": "esp32s3", + "USE_USB": "2", + "SDKCONFIGPATH": "./sdkconfig", + "PARTITIONS_FILE_FOR_TARGET": "./sdkconfig/partitions.csv", + "PROGRAMMING_MODE_MESSAGE": "INSTRUCTIONS: Press and hold the button until the LED lights.", + "BEFORE_DEBUGGING_MESSAGE": "Press and release the Reset button." + }, + "include": [ + "$(MODDABLE)/modules/io/manifest.json", + "$(MODDABLE)/modules/drivers/ili9341/manifest.json", + "$(MODDABLE)/modules/drivers/sensors/bmi270/manifest.json", + "$(MODULES)/drivers/button/manifest.json" + ], + "modules": { + "*": [ + "../m5stack_fire/m5button" + ], + "setup/target": "./setup-target" + }, + "preload": [ + "setup/target", + "m5button" + ], + "config": { + "screen": "ili9341", + "touch": "" + }, + "creation": { + "static": 0, + "chunk": { + "initial": 78848, + "incremental": 0 + }, + "heap": { + "initial": 4928, + "incremental": 0 + }, + "stack": 512 + }, + "defines": { + "i2c": { + "sda_pin": 45, + "scl_pin": 0 + }, + "spi": { + "mosi_pin":21, + "sck_pin": 15 + }, + "ili9341": { + "hz": 27000000, + "width": 128, + "height": 128, + "cs_pin": 14, + "rst_pin": 48, + "dc_pin": 42, + "column_offset": 0, + "row_offset": 32, + "spi_port": "SPI3_HOST", + "registers": [ + "0xFE, 0,", + "kDelayMS, 10,", + "0xEF, 0,", + "kDelayMS, 10,", + "0x36, 1, 0x08,", + "0xB0, 1, 0xC0,", + "0xB2, 1, 0x2F,", + "0xB3, 1, 0x03,", + "0xB6, 1, 0x19,", + "0xB7, 1, 0x01,", + "0xAC, 1, 0xCB,", + "0xAB, 1, 0x0E,", + "0xB4, 1, 0x04,", + "0xA8, 1, 0x19,", + "0x3A, 1, 0x05,", + "0xB8, 1, 0x08,", + "0xE8, 1, 0x24,", + "0xE9, 1, 0x48,", + "0xEA, 1, 0x22,", + "0xC6, 1, 0x30,", + "0xC7, 1, 0x18,", + "0xF0, 14, 0x1F,0x28,0x04,0x3E,0x2A,0x2E,0x20,0x00,0x0C,0x06,0x00,0x1C,0x1F,0x0F,", + "0xF1, 14, 0x00,0x2D,0x2F,0x3C,0x6F,0x1C,0x0B,0x00,0x00,0x00,0x07,0x0D,0x11,0x0F,", + "0x20, 0,", + "0x11, 0,", + "kDelayMS, 120,", + "0x29, 0,", + "kDelayMS, 20,", + "kDelayMS, 0" + ] + } + } +} diff --git a/build/devies/esp32/targets/m5atom_s3r_cam/host/provider.js b/build/devies/esp32/targets/m5atom_s3r_cam/host/provider.js new file mode 100644 index 0000000000..60dbb1a121 --- /dev/null +++ b/build/devies/esp32/targets/m5atom_s3r_cam/host/provider.js @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2021-2024 Moddable Tech, Inc. + * + * This file is part of the Moddable SDK Runtime. + * + * The Moddable SDK Runtime is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Moddable SDK Runtime is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with the Moddable SDK Runtime. If not, see . + * + */ + +import Analog from "embedded:io/analog"; +import Digital from "embedded:io/digital"; +import DigitalBank from "embedded:io/digitalbank"; +import I2C from "embedded:io/i2c"; +import PulseCount from "embedded:io/pulsecount"; +import PWM from "embedded:io/pwm"; +import Serial from "embedded:io/serial"; +import SMBus from "embedded:io/smbus"; +import SPI from "embedded:io/spi"; +import PulseWidth from "embedded:io/pulsewidth"; +import IMU from "embedded:sensor/Accelerometer-Gyroscope-Magnetometer/BMI270"; + +const device = { + I2C: { + default: { + io: I2C, + data: 45, + clock: 0, + }, + internal: { + io: I2C, + data: 45, + clock: 0, + } + }, + Serial: { + default: { + io: Serial, + port: 1, + receive: 44, + transmit: 43 + } + }, + SPI: { + default: { + io: SPI, + clock: 36, + in: 37, + out: 35, + port: 2 + } + }, + io: {Analog, Digital, DigitalBank, I2C, PulseCount, PulseWidth, PWM, Serial, SMBus, SPI}, + pin: { + //@@ button + button: 0 + }, + sensor: { + IMU: class { + constructor(options) { + return new IMU({ + ...options, + sensor: { + ...device.I2C.internal, + address: 0x68, + io: device.io.SMBus + } + }); + } + } + } +}; + +export default device; diff --git a/build/devies/esp32/targets/m5atom_s3r_cam/manifest.json b/build/devies/esp32/targets/m5atom_s3r_cam/manifest.json new file mode 100644 index 0000000000..f5b6e6ede5 --- /dev/null +++ b/build/devies/esp32/targets/m5atom_s3r_cam/manifest.json @@ -0,0 +1,63 @@ +{ + "build": { + "SDKCONFIGPATH": "./sdkconfig", + "PARTITIONS_FILE_FOR_TARGET": "./sdkconfig/partitions.csv", + "ESP32_SUBCLASS": "esp32s3", + "USE_USB": "2" + }, + "include": [ + "$(MODDABLE)/modules/io/manifest.json", + "$(MODDABLE)/modules/drivers/sensors/bmi270/manifest.json", + "$(MODULES)/drivers/button/manifest.json" + ], + "modules": { + "setup/target": "./setup-target" + }, + "preload": [ + "setup/target" + ], + "config": { + "Screen": "", + "button_pin": 41, + "power_pin": 18, + "ir_pin": 47 + }, + "creation": { + "static": 0, + "chunk": { + "initial": 78848, + "incremental": 0 + }, + "heap": { + "initial": 4928, + "incremental": 0 + }, + "stack": 512 + }, + "defines": { + "i2c": { + "sda_pin": 45, + "scl_pin": 0 + }, + "camera": { + "powerdown": -1, + "reset": -1, + "xclk": 21, + "pclk": 40, + "href": 14, + "vsync": 10, + "scl": 9, + "sda": 12, + "i2c_port": 1, + "d0": 3, + "d1": 42, + "d2": 46, + "d3": 48, + "d4": 4, + "d5": 17, + "d6": 11, + "d7": 13 + } + } +} + diff --git a/build/devies/esp32/targets/m5atom_s3r_m12/host/provider.js b/build/devies/esp32/targets/m5atom_s3r_m12/host/provider.js new file mode 100644 index 0000000000..60dbb1a121 --- /dev/null +++ b/build/devies/esp32/targets/m5atom_s3r_m12/host/provider.js @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2021-2024 Moddable Tech, Inc. + * + * This file is part of the Moddable SDK Runtime. + * + * The Moddable SDK Runtime is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Moddable SDK Runtime is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with the Moddable SDK Runtime. If not, see . + * + */ + +import Analog from "embedded:io/analog"; +import Digital from "embedded:io/digital"; +import DigitalBank from "embedded:io/digitalbank"; +import I2C from "embedded:io/i2c"; +import PulseCount from "embedded:io/pulsecount"; +import PWM from "embedded:io/pwm"; +import Serial from "embedded:io/serial"; +import SMBus from "embedded:io/smbus"; +import SPI from "embedded:io/spi"; +import PulseWidth from "embedded:io/pulsewidth"; +import IMU from "embedded:sensor/Accelerometer-Gyroscope-Magnetometer/BMI270"; + +const device = { + I2C: { + default: { + io: I2C, + data: 45, + clock: 0, + }, + internal: { + io: I2C, + data: 45, + clock: 0, + } + }, + Serial: { + default: { + io: Serial, + port: 1, + receive: 44, + transmit: 43 + } + }, + SPI: { + default: { + io: SPI, + clock: 36, + in: 37, + out: 35, + port: 2 + } + }, + io: {Analog, Digital, DigitalBank, I2C, PulseCount, PulseWidth, PWM, Serial, SMBus, SPI}, + pin: { + //@@ button + button: 0 + }, + sensor: { + IMU: class { + constructor(options) { + return new IMU({ + ...options, + sensor: { + ...device.I2C.internal, + address: 0x68, + io: device.io.SMBus + } + }); + } + } + } +}; + +export default device; diff --git a/build/devies/esp32/targets/m5atom_s3r_m12/manifest.json b/build/devies/esp32/targets/m5atom_s3r_m12/manifest.json new file mode 100644 index 0000000000..19781b65f2 --- /dev/null +++ b/build/devies/esp32/targets/m5atom_s3r_m12/manifest.json @@ -0,0 +1,64 @@ +{ + "build": { + "SDKCONFIGPATH": "./sdkconfig", + "PARTITIONS_FILE_FOR_TARGET": "./sdkconfig/partitions.csv", + "ESP32_SUBCLASS": "esp32s3", + "USE_USB": "2" + }, + "include": [ + "$(MODDABLE)/modules/io/manifest.json", + "$(MODDABLE)/modules/drivers/sensors/bmi270/manifest.json", + "$(MODULES)/drivers/button/manifest.json" + ], + "modules": { + "setup/target": "./setup-target" + }, + "preload": [ + "setup/target" + ], + "config": { + "Screen": "", + "button_pin": 41, + "power_pin": 18, + "ir_pin": 47 + }, + "creation": { + "static": 0, + "chunk": { + "initial": 78848, + "incremental": 0 + }, + "heap": { + "initial": 4928, + "incremental": 0 + }, + "stack": 512 + }, + "defines": { + "i2c": { + "sda_pin": 45, + "scl_pin": 0 + }, + "camera": { + "powerdown": -1, + "reset": -1, + "xclk": 21, + "xclk_freq_hz": 20000000, + "pclk": 40, + "href": 14, + "vsync": 10, + "scl": 9, + "sda": 12, + "i2c_port": 1, + "d0": 3, + "d1": 42, + "d2": 46, + "d3": 48, + "d4": 4, + "d5": 17, + "d6": 11, + "d7": 13 + } + } +} + diff --git a/examples/drivers/atoms3-imu/main.js b/examples/drivers/atoms3-imu/main.js index db59f5d0e2..ee6ee1e0e1 100644 --- a/examples/drivers/atoms3-imu/main.js +++ b/examples/drivers/atoms3-imu/main.js @@ -50,11 +50,10 @@ imu.configure({ }) Timer.repeat(() => { const sample = imu.sample(); - if(flag) { - onReading(sample.accelerometer, "a"); - } else { - onReading(sample.gyroscope, "g"); - } + const values = flag ? sample.accelerometer : sample.gyroscope; + + if (values) + onReading(values, flag ? "a" : "g"); }, 17) let flag = true; diff --git a/examples/drivers/co5300-rotation-benchmark/main.js b/examples/drivers/co5300-rotation-benchmark/main.js new file mode 100644 index 0000000000..fbaf9756c8 --- /dev/null +++ b/examples/drivers/co5300-rotation-benchmark/main.js @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2026 Moddable Tech, Inc. + * + * This file is part of the Moddable SDK. + * + * This work is licensed under the + * Creative Commons Attribution 4.0 International License. + * To view a copy of this license, visit + * . + * or send a letter to Creative Commons, PO Box 1866, + * Mountain View, CA 94042, USA. + * + */ + +import parseBMF from "commodetto/parseBMF"; +import Poco from "commodetto/Poco"; +import Resource from "Resource"; +import Timer from "timer"; + +const ROTATE_BUFFER_LINES = 16; +const SEND_LINES = 16; +const UPDATE_HEIGHT = 160; +const COLORS = [0xf800, 0x07e0, 0x001f, 0xffe0, 0x07ff, 0xf81f, 0xffff, 0x39e7]; + +const font = parseBMF(new Resource("OpenSans-Semibold-18.bf4")); +const tests = [ + { name: "rotation 0, full width", rotation: 0, width: "full", frames: 24 }, + { name: "rotation 90, full width", rotation: 90, width: "full", frames: 24 }, + { name: "rotation 90, narrow", rotation: 90, width: 96, frames: 36 }, + { name: "rotation 270, full width", rotation: 270, width: "full", frames: 24 }, + { name: "rotation 270, narrow", rotation: 270, width: 96, frames: 36 }, + { name: "rotation 180, full width", rotation: 180, width: "full", frames: 24 } +]; + +let testIndex = 0; +const bufferCache = new Map; + +trace("CO5300 rotation benchmark\n"); +trace("Build once before and after the driver change, then compare avg frame ms.\n"); + +Timer.set(runNextTest, 250); + +function runNextTest() { + const test = tests[testIndex++ % tests.length]; + const result = benchmark(test); + + trace(`${result.name}: avg=${result.average} ms/frame, total=${result.elapsed} ms, frames=${result.frames}, update=${result.updateWidth}x${result.updateHeight}, rotation=${result.rotation}`); + if (usesSoftwareRotation(result.rotation)) + trace(`, estimated chunks old=${result.oldChunks}, current=${result.currentChunks}`); + trace("\n"); + + drawSummary(result); + Timer.set(runNextTest, 2500); +} + +function benchmark(test) { + screen.rotation = test.rotation; + + const screenWidth = screen.width; + const screenHeight = screen.height; + const updateWidth = ("full" === test.width) ? screenWidth : Math.min(test.width, screenWidth); + const updateHeight = Math.min(UPDATE_HEIGHT, screenHeight); + const sendLines = Math.min(SEND_LINES, updateHeight); + const xRange = screenWidth - updateWidth; + const y = (screenHeight - updateHeight) >> 1; + const buffers = makePatternBuffers(updateWidth, sendLines); + + let start = Date.now(); + for (let frame = 0; frame < test.frames; frame++) { + const x = xRange ? ((frame * 17) % (xRange + 1)) : 0; + let lines = updateHeight; + let phase = frame & 3; + + screen.begin(x, y, updateWidth, updateHeight); + while (lines > 0) { + const rows = (lines > sendLines) ? sendLines : lines; + screen.send(buffers[phase].buffer, 0, updateWidth * rows * 2); + lines -= rows; + phase = (phase + 1) & 3; + } + screen.end(); + } + const elapsed = Date.now() - start; + + return { + name: test.name, + rotation: test.rotation, + frames: test.frames, + elapsed, + average: Math.round(elapsed / test.frames), + fps: Math.round((test.frames * 1000) / elapsed), + updateWidth, + updateHeight, + sendLines, + oldChunks: estimateChunks(updateWidth, updateHeight, sendLines, 2), + currentChunks: estimateChunks(updateWidth, updateHeight, sendLines, Math.max(1, Math.idiv((Math.max(screenWidth, screenHeight) * ROTATE_BUFFER_LINES), updateWidth))) + }; +} + +function estimateChunks(updateWidth, updateHeight, sendLines, rowsPerChunk) { + let chunks = 0; + for (let lines = updateHeight; lines > 0;) { + const rows = (lines > sendLines) ? sendLines : lines; + chunks += Math.idiv(rows + rowsPerChunk - 1, rowsPerChunk); + lines -= rows; + } + return chunks; +} + +function makePatternBuffers(width, lines) { + const key = `${width}:${lines}`; + let buffers = bufferCache.get(key); + if (buffers) + return buffers; + + buffers = []; + + for (let phase = 0; phase < 4; phase++) { + const pixels = new Uint16Array(new SharedArrayBuffer(width * lines * 2)); + for (let y = 0; y < lines; y++) { + for (let x = 0; x < width; x++) + pixels[(y * width) + x] = COLORS[((x >> 4) + (y >> 3) + phase) & 7]; + } + buffers.push(pixels); + } + + bufferCache.set(key, buffers); + return buffers; +} + +function drawSummary(result) { + screen.rotation = result.rotation; + + const render = new Poco(screen, { displayListLength: 4096 }); + try { + const black = render.makeColor(0, 0, 0); + const white = render.makeColor(255, 255, 255); + const green = render.makeColor(0, 220, 120); + const yellow = render.makeColor(255, 220, 0); + const cyan = render.makeColor(0, 180, 255); + const gray = render.makeColor(45, 45, 45); + + render.begin(); + render.fillRectangle(black, 0, 0, render.width, render.height); + render.fillRectangle(gray, 0, 0, render.width, 34); + drawText(render, "CO5300 rotation benchmark", white, 8, 8); + drawText(render, result.name, cyan, 8, 48); + drawText(render, `update ${result.updateWidth}x${result.updateHeight}, send ${result.sendLines} lines`, white, 8, 76); + drawText(render, `avg ${result.average} ms/frame, ${result.fps} fps`, green, 8, 104); + drawText(render, `total ${result.elapsed} ms / ${result.frames} frames`, white, 8, 132); + + if (usesSoftwareRotation(result.rotation)) { + const maxChunks = Math.max(result.oldChunks, result.currentChunks); + const oldWidth = Math.max(1, Math.idiv((render.width - 24) * result.oldChunks, maxChunks)); + const currentWidth = Math.max(1, Math.idiv((render.width - 24) * result.currentChunks, maxChunks)); + + drawText(render, `estimated chunks/frame old ${result.oldChunks}`, yellow, 8, 172); + render.fillRectangle(yellow, 8, 198, oldWidth, 16); + drawText(render, `estimated chunks/frame current ${result.currentChunks}`, green, 8, 224); + render.fillRectangle(green, 8, 250, currentWidth, 16); + } + else + drawText(render, "90/270 software rotation not used", yellow, 8, 172); + + render.end(); + } + finally { + render.close(); + } +} + +function drawText(render, text, color, x, y) { + render.drawText(text, font, color, x, y); +} + +function usesSoftwareRotation(rotation) { + return (90 === rotation) || (270 === rotation); +} diff --git a/examples/drivers/co5300-rotation-benchmark/manifest.json b/examples/drivers/co5300-rotation-benchmark/manifest.json new file mode 100644 index 0000000000..270ee49ece --- /dev/null +++ b/examples/drivers/co5300-rotation-benchmark/manifest.json @@ -0,0 +1,12 @@ +{ + "include": [ + "$(MODDABLE)/examples/manifest_base.json", + "$(MODDABLE)/examples/manifest_commodetto.json" + ], + "modules": { + "*": "./main" + }, + "resources": { + "*-mask": "$(MODDABLE)/examples/assets/fonts/OpenSans-Semibold-18" + } +} diff --git a/examples/drivers/sensors/bmi270/poll/main.js b/examples/drivers/sensors/bmi270/poll/main.js new file mode 100644 index 0000000000..d655a5951d --- /dev/null +++ b/examples/drivers/sensors/bmi270/poll/main.js @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026 Moddable Tech, Inc. + * + * This file is part of the Moddable SDK. + * + * This work is licensed under the + * Creative Commons Attribution 4.0 International License. + * To view a copy of this license, visit + * . + * or send a letter to Creative Commons, PO Box 1866, + * Mountain View, CA 94042, USA. + * + */ + +import BMI270 from "embedded:sensor/Accelerometer-Gyroscope-Magnetometer/BMI270"; +import Timer from "timer"; + +const i2c = device.I2C.internal ?? device.I2C.default; + +const sensor = new BMI270({ + sensor: { + ...i2c, + io: device.io.SMBus + } +}); + +Timer.repeat(() => { + const sample = sensor.sample(); + + trace("Accel: "); + traceVector(sample.accelerometer); + trace(" - Gyro: "); + traceVector(sample.gyroscope); + + if (sample.thermometer) + trace(` - Temp: ${sample.thermometer.temperature.toFixed(2)} C`); + + trace("\n"); +}, 1000); + +function traceVector(values) { + if (!values) { + trace("[pending]"); + return; + } + + trace(`[${format(values.x)}, ${format(values.y)}, ${format(values.z)}]`); +} + +function format(value) { + return (undefined === value) ? "n/a" : value.toFixed(3); +} diff --git a/examples/drivers/sensors/bmi270/poll/manifest.json b/examples/drivers/sensors/bmi270/poll/manifest.json new file mode 100644 index 0000000000..4fa271040a --- /dev/null +++ b/examples/drivers/sensors/bmi270/poll/manifest.json @@ -0,0 +1,10 @@ +{ + "include": [ + "$(MODDABLE)/examples/manifest_base.json", + "$(MODDABLE)/modules/io/manifest.json", + "$(MODDABLE)/modules/drivers/sensors/bmi270/manifest.json" + ], + "modules": { + "*": "./main" + } +} diff --git a/modules/drivers/co5300/co5300.js b/modules/drivers/co5300/co5300.js index 03057c3c91..b5e66843fd 100644 --- a/modules/drivers/co5300/co5300.js +++ b/modules/drivers/co5300/co5300.js @@ -38,6 +38,8 @@ export default class CO5300 @ "xs_co5300_destructor" { get width() @ "xs_co5300_get_width"; get height() @ "xs_co5300_get_height"; get async() {return true;} + get rotation() @ "xs_co5300_get_rotation"; + set rotation(value) @ "xs_co5300_set_rotation"; get c_dispatch() @ "xs_co5300_get_c_dispatch"; diff --git a/modules/drivers/co5300/modCo5300.c b/modules/drivers/co5300/modCo5300.c index 04f2112ed4..4ab0dcb114 100644 --- a/modules/drivers/co5300/modCo5300.c +++ b/modules/drivers/co5300/modCo5300.c @@ -76,6 +76,10 @@ */ #define CO5300_CMD(reg) ((0x02 << 24) | ((reg) << 8)) #define CO5300_COLOR_CMD ((0x32 << 24) | (0x2C << 8)) +#define CO5300_MADCTL_MY (0x80) +#define CO5300_MADCTL_MX (0x40) +#define CO5300_ROTATE_BUFFER_LINES 16 +#define CO5300_ROTATE_BUFFER_PIXELS ((((MODDEF_CO5300_WIDTH) > (MODDEF_CO5300_HEIGHT)) ? (MODDEF_CO5300_WIDTH) : (MODDEF_CO5300_HEIGHT)) * CO5300_ROTATE_BUFFER_LINES) typedef struct { PixelsOutDispatch dispatch; @@ -85,6 +89,9 @@ typedef struct { #endif int updateWidth; + int updateHeight; + int updateX; + int updateY; int updateLinesRemaining; int yMin; int yMax; @@ -92,6 +99,8 @@ typedef struct { uint8_t nothingSent; uint8_t firstFrame; uint8_t brightnessValue; + uint8_t rotation; // 0, 1, 2, 3 => 0, 90, 180, 270 + uint8_t colorSlotReserved; SemaphoreHandle_t colorsInFlight; esp_lcd_panel_io_handle_t io_handle; @@ -100,9 +109,17 @@ typedef struct { int opZero; uint8_t data[32]; + uint16_t *rotateBuffer; } co5300DisplayRecord, *co5300Display; static void co5300Init(co5300Display sd); +static void co5300SetMADCTL(co5300Display sd); +static void co5300SetWindow(co5300Display sd, uint16_t x, uint16_t y, uint16_t w, uint16_t h); +static void co5300WaitForColors(co5300Display sd); +static void co5300SendColorSync(co5300Display sd, const void *pixels, int byteLength); +static uint8_t co5300EnsureRotateBuffer(co5300Display sd); +static void co5300SendRotated(co5300Display sd, uint16_t *pixels, int byteLength); +static void co5300SendHardwareRotated180(co5300Display sd, uint16_t *pixels, int byteLength); #define co5300Command(sd, command, data, count) \ (esp_lcd_panel_io_tx_param(sd->io_handle, command, data, count)) @@ -143,6 +160,9 @@ void xs_co5300_destructor(void *data) if (sd->colorsInFlight) vSemaphoreDelete(sd->colorsInFlight); + if (sd->rotateBuffer) + c_free(sd->rotateBuffer); + c_free(data); } @@ -189,6 +209,7 @@ void xs_co5300(xsMachine *the) int err = spi_bus_initialize(MODDEF_CO5300_SPI_PORT, &buscfg, SPI_DMA_CH_AUTO); if (err) { + c_free(sd->rotateBuffer); c_free(sd); xsmcSetHostData(xsThis, NULL); xsUnknownError("spi_bus_initialize failed"); @@ -212,6 +233,7 @@ void xs_co5300(xsMachine *the) err = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)MODDEF_CO5300_SPI_PORT, &io_config, &sd->io_handle); if (err) { spi_bus_free(MODDEF_CO5300_SPI_PORT); + c_free(sd->rotateBuffer); c_free(sd); xsmcSetHostData(xsThis, NULL); xsUnknownError("esp_lcd_new_panel_io_spi failed"); @@ -295,12 +317,36 @@ void xs_co5300_get_pixelFormat(xsMachine *the) void xs_co5300_get_width(xsMachine *the) { - xsmcSetInteger(xsResult, MODDEF_CO5300_WIDTH); + co5300Display sd = xsmcGetHostData(xsThis); + xsmcSetInteger(xsResult, (sd->rotation & 1) ? MODDEF_CO5300_HEIGHT : MODDEF_CO5300_WIDTH); } void xs_co5300_get_height(xsMachine *the) { - xsmcSetInteger(xsResult, MODDEF_CO5300_HEIGHT); + co5300Display sd = xsmcGetHostData(xsThis); + xsmcSetInteger(xsResult, (sd->rotation & 1) ? MODDEF_CO5300_WIDTH : MODDEF_CO5300_HEIGHT); +} + +void xs_co5300_get_rotation(xsMachine *the) +{ + co5300Display sd = xsmcGetHostData(xsThis); + xsmcSetInteger(xsResult, sd->rotation * 90); +} + +void xs_co5300_set_rotation(xsMachine *the) +{ + co5300Display sd = xsmcGetHostData(xsThis); + int32_t rotation = xsmcToInteger(xsArg(0)); + uint8_t newRotation; + + if ((0 != rotation) && (90 != rotation) && (180 != rotation) && (270 != rotation)) + xsRangeError("invalid rotation"); + + newRotation = (uint8_t)(rotation / 90); + if ((newRotation & 1) && !co5300EnsureRotateBuffer(sd)) + xsUnknownError("no memory"); + sd->rotation = newRotation; + co5300SetMADCTL(sd); } void xs_co5300_get_c_dispatch(xsMachine *the) @@ -365,8 +411,19 @@ void co5300Send(PocoPixel *pixels, int byteLength, void *refcon) } #endif + if (1 == (sd->rotation & 1)) { + co5300SendRotated(sd, (uint16_t *)pixels, byteLength); + return; + } + + if (2 == sd->rotation) { + co5300SendHardwareRotated180(sd, (uint16_t *)pixels, byteLength); + return; + } + { int one = 1; + sd->colorSlotReserved = 0; xQueueSend(sd->ops, &one, portMAX_DELAY); esp_lcd_panel_io_tx_color(sd->io_handle, CO5300_COLOR_CMD, pixels, byteLength); @@ -401,6 +458,7 @@ static const uint8_t gInit[] ICACHE_RODATA_ATTR = { 0xFE, 1, 0x00, // Page select (page 0) 0xC4, 1, 0x80, // Set interface (QSPI) 0x3A, 1, 0x55, // Pixel format: 16-bit RGB565 + 0x36, 1, 0x00, // Memory data access control 0x44, 2, 0x01, 0xD1, // Set tear scanline 0x35, 1, 0x00, // Tearing effect line on 0x53, 1, 0x20, // Write CTRL display value @@ -440,6 +498,116 @@ void co5300Init(co5300Display sd) sd->firstFrame = true; sd->brightnessValue = 0; + sd->rotation = 0; +} + +static void co5300SetMADCTL(co5300Display sd) +{ + uint8_t value = (2 == sd->rotation) ? (CO5300_MADCTL_MY | CO5300_MADCTL_MX) : 0; + co5300Command(sd, CO5300_CMD(0x36), &value, 1); +} + +static void co5300SetWindow(co5300Display sd, uint16_t x, uint16_t y, uint16_t w, uint16_t h) +{ + uint16_t xMin = x + MODDEF_CO5300_COLUMN_OFFSET; + uint16_t yMin = y + MODDEF_CO5300_ROW_OFFSET; + uint16_t xMax = xMin + w - 1; + uint16_t yMax = yMin + h - 1; + uint8_t data[4]; + + data[0] = (xMin >> 8) & 0xff; + data[1] = xMin & 0xff; + data[2] = (xMax >> 8) & 0xff; + data[3] = xMax & 0xff; + co5300Command(sd, CO5300_CMD(0x2a), data, 4); + + data[0] = (yMin >> 8) & 0xff; + data[1] = yMin & 0xff; + data[2] = (yMax >> 8) & 0xff; + data[3] = yMax & 0xff; + co5300Command(sd, CO5300_CMD(0x2b), data, 4); +} + +static void co5300WaitForColors(co5300Display sd) +{ + xSemaphoreTake(sd->colorsInFlight, portMAX_DELAY); + xSemaphoreTake(sd->colorsInFlight, portMAX_DELAY); + xSemaphoreGive(sd->colorsInFlight); + xSemaphoreGive(sd->colorsInFlight); +} + +static void co5300SendColorSync(co5300Display sd, const void *pixels, int byteLength) +{ + int one = 1; + + if (sd->colorSlotReserved) + sd->colorSlotReserved = 0; + else + xSemaphoreTake(sd->colorsInFlight, portMAX_DELAY); + + xQueueSend(sd->ops, &one, portMAX_DELAY); + esp_lcd_panel_io_tx_color(sd->io_handle, CO5300_COLOR_CMD, pixels, byteLength); + co5300WaitForColors(sd); +} + +static uint8_t co5300EnsureRotateBuffer(co5300Display sd) +{ + if (!sd->rotateBuffer) + sd->rotateBuffer = c_malloc(CO5300_ROTATE_BUFFER_PIXELS * sizeof(uint16_t)); + return NULL != sd->rotateBuffer; +} + +static void co5300SendRotated(co5300Display sd, uint16_t *pixels, int byteLength) +{ + int lines = (byteLength >> 1) / sd->updateWidth; + int row = sd->updateHeight - sd->updateLinesRemaining; + uint16_t *src = pixels; + int rowsPerBuffer = CO5300_ROTATE_BUFFER_PIXELS / sd->updateWidth; + + while (lines > 0) { + int rows = (lines > rowsPerBuffer) ? rowsPerBuffer : lines; + uint16_t *out = sd->rotateBuffer; + uint16_t x, y, w, h; + + if (1 == sd->rotation) { + for (int col = 0; col < sd->updateWidth; col++) { + for (int r = rows - 1; r >= 0; r--) + *out++ = src[(r * sd->updateWidth) + col]; + } + x = MODDEF_CO5300_WIDTH - (sd->updateY + row + rows); + y = sd->updateX; + } + else { + for (int col = sd->updateWidth - 1; col >= 0; col--) { + for (int r = 0; r < rows; r++) + *out++ = src[(r * sd->updateWidth) + col]; + } + x = sd->updateY + row; + y = MODDEF_CO5300_HEIGHT - (sd->updateX + sd->updateWidth); + } + w = rows; + h = sd->updateWidth; + + co5300SetWindow(sd, x, y, w, h); + co5300SendColorSync(sd, sd->rotateBuffer, (int)((out - sd->rotateBuffer) * sizeof(uint16_t))); + + src += rows * sd->updateWidth; + row += rows; + lines -= rows; + sd->updateLinesRemaining -= rows; + } +} + +static void co5300SendHardwareRotated180(co5300Display sd, uint16_t *pixels, int byteLength) +{ + int lines = (byteLength >> 1) / sd->updateWidth; + int row = sd->updateHeight - sd->updateLinesRemaining; + uint16_t x = MODDEF_CO5300_WIDTH - (sd->updateX + sd->updateWidth); + uint16_t y = MODDEF_CO5300_HEIGHT - (sd->updateY + row + lines); + + co5300SetWindow(sd, x, y, sd->updateWidth, lines); + co5300SendColorSync(sd, pixels, byteLength); + sd->updateLinesRemaining -= lines; } void co5300AdaptInvalid(void *refcon, CommodettoRectangle r) @@ -460,37 +628,30 @@ void co5300AdaptInvalid(void *refcon, CommodettoRectangle r) void co5300Begin(void *refcon, CommodettoCoordinate x, CommodettoCoordinate y, CommodettoDimension w, CommodettoDimension h) { co5300Display sd = refcon; - uint16_t xMin, xMax, yMin, yMax; + uint16_t yMin, yMax; - if (sd->nothingSent) + if (sd->nothingSent && sd->colorSlotReserved) { xSemaphoreGive(sd->colorsInFlight); + sd->colorSlotReserved = 0; + } sd->nothingSent = 1; - xMin = x + MODDEF_CO5300_COLUMN_OFFSET; yMin = y + MODDEF_CO5300_ROW_OFFSET; - - xMax = xMin + w - 1; yMax = yMin + h - 1; + sd->updateX = x; + sd->updateY = y; sd->updateWidth = w; + sd->updateHeight = h; sd->updateLinesRemaining = h; sd->yMin = yMin; sd->yMax = yMax; - uint8_t data[4]; - data[0] = (xMin >> 8) & 0xff; - data[1] = xMin & 0xff; - data[2] = (xMax >> 8) & 0xff; - data[3] = xMax & 0xff; - co5300Command(sd, CO5300_CMD(0x2a), data, 4); - - data[0] = (yMin >> 8) & 0xff; - data[1] = yMin & 0xff; - data[2] = (yMax >> 8) & 0xff; - data[3] = yMax & 0xff; - co5300Command(sd, CO5300_CMD(0x2b), data, 4); + if (!sd->rotation) + co5300SetWindow(sd, x, y, w, h); xSemaphoreTake(sd->colorsInFlight, portMAX_DELAY); + sd->colorSlotReserved = 1; } void co5300Continue(void *refcon) @@ -512,7 +673,10 @@ void co5300End(void *refcon) } if (sd->nothingSent) { - xSemaphoreGive(sd->colorsInFlight); + if (sd->colorSlotReserved) { + xSemaphoreGive(sd->colorsInFlight); + sd->colorSlotReserved = 0; + } sd->nothingSent = 0; } }