From 584b68b7b12f5cc447a6dc530eb38be4d9d70917 Mon Sep 17 00:00:00 2001 From: Tage Mellemstrand Date: Wed, 25 Mar 2026 23:18:03 +0100 Subject: [PATCH] Add support for reading and writing to I2C devices from lua (#934) * Add i2c read support to lua * Add i2c write support to lua With memory permissions before write * Adjust i2c write * Fix inverted id check, better permissions * Change write to not return anything on success * Add some documentation for lua i2c * Change write length from 1024 to 64 * Add whitelist for i2c writing * Move i2c module into preload * Add registers and bitmasks for mcu Add documentation to lua-doc * Fix missing column divider in lua-doc * Add ~= 0 then to lua example * Add some more registers --- arm9/source/lua/gm9internali2c.c | 147 +++++++++++++++ arm9/source/lua/gm9internali2c.h | 6 + arm9/source/lua/gm9lua.c | 8 + arm9/source/lua/gm9lua.h | 1 + data/luapackages/i2c.lua | 240 ++++++++++++++++++++++++ data/preload.lua | 1 + resources/lua-doc.md | 312 +++++++++++++++++++++++++++++++ 7 files changed, 715 insertions(+) create mode 100644 arm9/source/lua/gm9internali2c.c create mode 100644 arm9/source/lua/gm9internali2c.h create mode 100644 data/luapackages/i2c.lua diff --git a/arm9/source/lua/gm9internali2c.c b/arm9/source/lua/gm9internali2c.c new file mode 100644 index 0000000..d7837d2 --- /dev/null +++ b/arm9/source/lua/gm9internali2c.c @@ -0,0 +1,147 @@ +#ifndef NO_LUA +#include "gm9internali2c.h" +#include "fsperm.h" +#include "system/i2c.h" + +typedef struct { + uint8_t dev_id; + uint8_t reg_addr_start; + uint8_t reg_addr_end; +} WhiteListPair; + +static const WhiteListPair WriteWhitelist[] = { + // Device_id, Register_address, Register_address_end + { I2C_DEV_MCU, 0x15, 0x17 }, + { I2C_DEV_MCU, 0x1c, 0x1f }, + { I2C_DEV_MCU, 0x28, 0x29 }, + { I2C_DEV_MCU, 0x2a, 0x2d }, +}; + +static bool IsWriteAllowed(uint8_t dev_id, uint8_t reg_addr) +{ + for (unsigned i = 0; i < sizeof(WriteWhitelist)/sizeof(WriteWhitelist[0]); i++) { + if (WriteWhitelist[i].dev_id == dev_id && + reg_addr >= WriteWhitelist[i].reg_addr_start && + reg_addr <= WriteWhitelist[i].reg_addr_end) + return true; + } + return false; +} + +static int i2c_read(lua_State *L) { + int dev_id = luaL_checkinteger(L, 1); + int reg_addr = luaL_checkinteger(L, 2); + int length = luaL_checkinteger(L, 3); + + if (dev_id < 0 || dev_id > 17) { + return luaL_error(L, "Invalid device ID: %d (must be 0-17)", dev_id); + } + + if (length <= 0 || length > 64) { + return luaL_error(L, "Invalid length: %d (must be 1-64)", length); + } + + if (reg_addr < 0 || reg_addr > 255) { + return luaL_error(L, "Invalid register address: %d (must be 0-255)", reg_addr); + } + + // Create a buffer for the read data + u8 *buffer = malloc(length); + if (!buffer) { + return luaL_error(L, "Memory allocation failed"); + } + bool success = I2C_readRegBuf((I2cDevice)dev_id, (u8)reg_addr, buffer, (u32)length); + + if (success) { + // Create a Lua table to hold the byte values + lua_createtable(L, length, 0); + + // Fill the table with byte values as integers + for (int i = 0; i < length; i++) { + lua_pushinteger(L, buffer[i]); // Push byte value as integer + lua_rawseti(L, -2, i + 1); // Set table[i+1] = byte + } + + free(buffer); + return 1; + } else { + free(buffer); + return luaL_error(L, "I2C read failed"); + } +} + +static int i2c_write(lua_State *L) { + SetWritePermissionsLuaError(L, PERM_MEMORY); + + int dev_id = luaL_checkinteger(L, 1); + int reg_addr = luaL_checkinteger(L, 2); + + if (!lua_istable(L, 3)) { + return luaL_error(L, "Third parameter must be a table of byte values"); + } + + int length = lua_rawlen(L, 3); + + if (dev_id < 0 || dev_id > 17) { + return luaL_error(L, "Invalid device ID: %d (must be 0-17)", dev_id); + } + + if (length <= 0 || length > 64) { + return luaL_error(L, "Invalid data length: %d (must be 1-64)", length); + } + + if (reg_addr < 0 || reg_addr > 255) { + return luaL_error(L, "Invalid register address: %d (must be 0-255)", reg_addr); + } + + if (!IsWriteAllowed(dev_id, reg_addr)) { + return luaL_error(L, "Write to device %d, register %d is not allowed", dev_id, reg_addr); + } + + // Create a buffer for the write data + u8 *buffer = malloc(length); + if (!buffer) { + return luaL_error(L, "Memory allocation failed"); + } + + // Extract byte values from Lua table + for (int i = 0; i < length; i++) { + lua_rawgeti(L, 3, i + 1); // Get table[i+1] + + if (!lua_isinteger(L, -1)) { + free(buffer); + return luaL_error(L, "Table element %d is not an integer", i + 1); + } + + int value = lua_tointeger(L, -1); + if (value < 0 || value > 255) { + free(buffer); + return luaL_error(L, "Table element %d is out of range: %d (must be 0-255)", i + 1, value); + } + + buffer[i] = (u8)value; + lua_pop(L, 1); // Remove the value from stack + } + + bool success = I2C_writeRegBuf((I2cDevice)dev_id, (u8)reg_addr, buffer, (u32)length); + + free(buffer); + + if (success) { + return 0; + } else { + return luaL_error(L, "I2C write failed"); + } +} + +static const luaL_Reg i2c[] = { + {"read", i2c_read}, + {"write", i2c_write}, + {NULL, NULL} +}; + +int gm9lua_open_internali2c(lua_State* L) { + luaL_newlib(L, i2c); + return 1; +} +#endif diff --git a/arm9/source/lua/gm9internali2c.h b/arm9/source/lua/gm9internali2c.h new file mode 100644 index 0000000..1685ecf --- /dev/null +++ b/arm9/source/lua/gm9internali2c.h @@ -0,0 +1,6 @@ +#pragma once +#include "gm9lua.h" + +#define GM9LUA_I2CLIBNAME "_i2c" + +int gm9lua_open_internali2c(lua_State* L); diff --git a/arm9/source/lua/gm9lua.c b/arm9/source/lua/gm9lua.c index 32f8bb2..bb95c9f 100644 --- a/arm9/source/lua/gm9lua.c +++ b/arm9/source/lua/gm9lua.c @@ -13,6 +13,7 @@ #include "gm9os.h" #include "gm9ui.h" #include "gm9title.h" +#include "gm9internali2c.h" #include "gm9internalfs.h" #include "gm9internalsys.h" @@ -90,6 +91,12 @@ void CheckWritePermissionsLuaError(lua_State* L, const char* path) { } } +void SetWritePermissionsLuaError(lua_State* L, u32 perm) { + if (!SetWritePermissions(perm, true)) { + luaL_error(L, "failed to set write permissions"); + } +} + static const luaL_Reg gm9lualibs[] = { // built-ins {LUA_GNAME, luaopen_base}, @@ -109,6 +116,7 @@ static const luaL_Reg gm9lualibs[] = { // gm9 custom internals (usually wrapped by a pure lua module) {GM9LUA_INTERNALFSLIBNAME, gm9lua_open_internalfs}, {GM9LUA_INTERNALSYSLIBNAME, gm9lua_open_internalsys}, + {GM9LUA_I2CLIBNAME, gm9lua_open_internali2c}, {NULL, NULL} }; diff --git a/arm9/source/lua/gm9lua.h b/arm9/source/lua/gm9lua.h index 58065d8..6f1f835 100644 --- a/arm9/source/lua/gm9lua.h +++ b/arm9/source/lua/gm9lua.h @@ -45,5 +45,6 @@ static inline bool CheckLuaArgCountPlusExtra(lua_State* L, int argcount, const c int LoadLuaFile(lua_State* L, const char* filename); u32 GetFlagsFromTable(lua_State* L, int pos, u32 flags_ext_starter, u32 allowed_flags); void CheckWritePermissionsLuaError(lua_State* L, const char* path); +void SetWritePermissionsLuaError(lua_State* L, u32 perm); #endif bool ExecuteLuaScript(const char* path_script); diff --git a/data/luapackages/i2c.lua b/data/luapackages/i2c.lua new file mode 100644 index 0000000..410893a --- /dev/null +++ b/data/luapackages/i2c.lua @@ -0,0 +1,240 @@ +local i2c = {} + +i2c.read = _i2c.read +i2c.write = _i2c.write + +-- List of devices on the I2C bus +i2c.dev = { + POWER = 0, -- Unconfirmed + CAMERA = 1, -- Unconfirmed + CAMERA2 = 2, -- Unconfirmed + MCU = 3, + CAMERA3 = 4, + LCD1 = 5, + LCD2 = 6, + GYRO3 = 9, + GYRO = 10, + GYRO2 = 11, + DEBUG_PAD = 12, + IR = 13, + EEPROM = 14, -- Unconfirmed + NFC = 15, + QTM = 16, + N3DS_HID = 17 +} + +-- Sub-tables for specific devices, containing register addresses and some bitmasks +i2c.mcu = { + -- Bitmasks for the 4-byte interupt registers. + interrupt_1 = { + POWER_BUTTON_HELD = 1 << 1, -- The power button was held. + HOME_BUTTON_PRESS = 1 << 2, -- The HOME button was pressed. + HOME_BUTTON_RELEASE = 1 << 3, -- The HOME button was released. + WLAN_SWITCH_TRIGGER = 1 << 4, -- The WiFi switch was triggered. + SHELL_CLOSE = 1 << 5, -- The shell was closed (or sleep switch turned off). + SHELL_OPEN = 1 << 6, -- The shell was opened (or sleep switch turned on). + FATAL_HW_ERROR = 1 << 7, -- MCU watchdog reset occurred. + }, + + interrupt_2 = { + AC_ADAPTER_REMOVED = 1 << 0, -- The AC adapter was disconnected. + AC_ADAPTER_PLUGGED_IN = 1 << 1, -- The AC adapter was connected. + RTC_ALARM = 1 << 2, -- The RTC alarm time has been reached. + ACCELEROMETER_I2C_MANUAL_IO = 1 << 3, -- The manual accelerometer I²C read/write operation has completed. + ACCELEROMETER_NEW_SAMPLE = 1 << 4, -- A new accelerometer sample is available. + CRITICAL_BATTERY = 1 << 5, -- The battery is critically low. + CHARGING_STOP = 1 << 6, -- The battery stopped charging. + CHARGING_START = 1 << 7, -- The battery started charging. + }, + + interrupt_3 = { + VOL_SLIDER = 1 << 6, -- The position of the volume slider changed. + }, + + interrupt_4 = { + LCD_OFF = 1 << 0, -- The LCDs turned off. + LCD_ON = 1 << 1, -- The LCDs turned on. + BOT_BACKLIGHT_OFF = 1 << 2, -- The bottom screen backlight turned off. + BOT_BACKLIGHT_ON = 1 << 3, -- The bottom screen backlight turned on. + TOP_BACKLIGHT_OFF = 1 << 4, -- The top screen backlight turned off. + TOP_BACKLIGHT_ON = 1 << 5, -- The top screen backlight turned on. + }, + + -- Register addresses + reg = { + VERSION_HIGH = 0x0, -- Major version of the MCU firmware. + VERSION_LOW = 0x1, -- Minor version of the MCU firmware. + + RESET_EVENTS = 0x2, -- @ref mcu.reset_event_flags + + VCOM_TOP = 0x3, -- Flicker/VCOM value for the top screen. + VCOM_BOTTOM = 0x4, -- Flicker/VCOM value for the bottom screen. + + FIRMWARE_UPLOAD_0 = 0x5, -- Firmware upload register. + FIRMWARE_UPLOAD_1 = 0x6, -- Firmware upload register. + FIRMWARE_UPLOAD_2 = 0x7, -- Firmware upload register. + + RAW_3D_SLIDER_POSITION = 0x8, -- Position of the 3D slider. + VOLUME_SLIDER_POSITION = 0x9, -- Position of the volume slider. + + BATTERY_PCB_TEMPERATURE = 0xA, -- Temperature of the battery, measured on a sensor on the PCB. + BATTERY_PERCENTAGE_INT = 0xB, -- Integer part of the battery percentage. + BATTERY_PERCENTAGE_FRAC = 0xC, -- Fractional part of the battery percentage. + BATTERY_VOLTAGE = 0xD, -- Voltage of the battery, in units of 20 mV. + + HW_STATUS = 0xE, -- Hardware status bits + POWER_STATUS = 0xF, -- @ref mcu.power_status_flags + + LEGACY_VERSION_HIGH = 0xF, -- (Old MCU_FIRM only) Major firmware version. + LEGACY_VERSION_LOW = 0x10, -- (Old MCU_FIRM only) Minor firmware version. + LEGACY_FIRM_UPLOAD = 0x3B, -- (Old MCU_FIRM only) Firmware upload register. + + RECEIVED_IRQS_1 = 0x10, -- Bitmask of received IRQs. @ref mcu.interrupt_1 + RECEIVED_IRQS_2 = 0x11, -- Bitmask of received IRQs. @ref mcu.interrupt_2 + RECEIVED_IRQS_3 = 0x12, -- Bitmask of received IRQs. @ref mcu.interrupt_3 + RECEIVED_IRQS_4 = 0x13, -- Bitmask of received IRQs. @ref mcu.interrupt_4 + UNUSED_RO_14 = 0x14, -- Unused read-only register. + USERDATA_RAM_15 = 0x15, -- Unused register, supports writing + USERDATA_RAM_16 = 0x16, -- Unused register, supports writing + USERDATA_RAM_17 = 0x17, -- Unused register, supports writing + IRQ_MASK_1 = 0x18, -- Bitmask of enabled IRQs. @ref mcu.interrupt_1 + IRQ_MASK_2 = 0x19, -- Bitmask of enabled IRQs. @ref mcu.interrupt_2 + IRQ_MASK_3 = 0x1A, -- Bitmask of enabled IRQs. @ref mcu.interrupt_3 + IRQ_MASK_4 = 0x1B, -- Bitmask of enabled IRQs. @ref mcu.interrupt_4 + USERDATA_RAM_1C = 0x1C, -- Unused register, supports writing + USERDATA_RAM_1D = 0x1D, -- Unused register, supports writing + USERDATA_RAM_1E = 0x1E, -- Unused register, supports writing + USERDATA_RAM_1F = 0x1F, -- Unused register, supports writing + + PWR_CTL = 0x20, -- @ref mcu.power_trigger + LCD_PWR_CTL = 0x22, -- LCD Power control. + MCU_RESET_CTL = 0x23, -- Writing 'r' to this register resets the MCU. Stubbed on retail. + FORCE_SHUTDOWN_DELAY = 0x24, -- The amount of time, in units of 0.125s, for which the power button needs to be held to trigger a hard shutdown. + + VOLUME_UNK_25 = 0x25, -- Unknown register. Used in mcu::SND + UNK_26 = 0x26, -- Unknown register. Used in mcu::CDC + + VOLUME_SLIDER_RAW = 0x27, -- Raw value of the volume slider, in the range of 0-255 + LED_BRIGHTNESS_STATE = 0x28, -- Brightness of the status LEDs. + POWER_LED_STATE = 0x29, -- @ref mcu.power_led_state + WLAN_LED_STATE = 0x2A, -- Controls the WiFi LED. + CAMERA_LED_STATE = 0x2B, -- Controls the camera LED (on models that have one). + LED_3D_STATE = 0x2C, -- Controls the 3D LED (on models that have one). + NOTIFICATION_LED_STATE = 0x2D, -- Controls the info (notification) LED. + NOTIFICATION_LED_CYCLE_STATE = 0x2E, -- Bit 0 is set if the info (notification) LED has started a new cycle of its pattern. + + RTC_TIME_SECOND = 0x30, -- RTC second. + RTC_TIME_MINUTE = 0x31, -- RTC minute. + RTC_TIME_HOUR = 0x32, -- RTC hour. + RTC_TIME_WEEKDAY = 0x33, -- RTC day of the week. + RTC_TIME_DAY = 0x34, -- RTC day of the month. + RTC_TIME_MONTH = 0x35, -- RTC month. + RTC_TIME_YEAR = 0x36, -- RTC year. + RTC_TIME_CORRECTION = 0x37, -- RTC subsecond (RSUBC) correction value. + + RTC_ALARM_MINUTE = 0x38, -- RTC alarm minute. + RTC_ALARM_HOUR = 0x39, -- RTC alarm hour. + RTC_ALARM_DAY = 0x3A, -- RTC alarm day. + RTC_ALARM_MONTH = 0x3B, -- RTC alarm month. + RTC_ALARM_YEAR = 0x3C, -- RTC alarm year. + + TICK_COUNTER_LSB = 0x3D, -- MCU tick counter value (low byte). + TICK_COUNTER_MSB = 0x3E, -- MCU tick counter value (high byte). + + UNK_3F = 0x3F, -- Unknown register + + SENSOR_CONFIG = 0x40, -- @ref mcu.sensor_config + + ACCELEROMETER_MANUAL_REGID_R = 0x41, -- Hardware register ID for use in manual accelerometer I²C reads. + ACCELEROMETER_MANUAL_REGID_W = 0x43, -- Hardware reigster ID for use in manual accelerometer I²C writes. + ACCELEROMETER_MANUAL_IO = 0x44, -- Data register for manual accelerometer reads/writes. + ACCELEROMETER_OUTPUT_X_LSB = 0x45, -- Accelerometer X coordinate (low byte). + ACCELEROMETER_OUTPUT_X_MSB = 0x46, -- Accelerometer X coordinate (high byte). + ACCELEROMETER_OUTPUT_Y_LSB = 0x47, -- Accelerometer Y coordinate (low byte). + ACCELEROMETER_OUTPUT_Y_MSB = 0x48, -- Accelerometer Y coordinate (high byte). + ACCELEROMETER_OUTPUT_Z_LSB = 0x49, -- Accelerometer Z coordinate (low byte). + ACCELEROMETER_OUTPUT_Z_MSB = 0x4A, -- Accelerometer Z coordinate (high byte). + + PEDOMETER_STEPS_LOWBYTE = 0x4B, -- Pedometer step count (low byte). + PEDOMETER_STEPS_MIDDLEBYTE = 0x4C, -- Pedometer step count (middle byte). + PEDOMETER_STEPS_HIGHBYTE = 0x4D, -- Pedometer step count (high byte). + PEDOMETER_CNT = 0x4E, -- @ref mcu.pedometer_control + PEDOMETER_STEP_DATA = 0x4F, -- Step history and time of last update. + PEDOMETER_WRAP_MINUTE = 0x50, -- The minute within each RTC hour at which the step history should roll into the next hour. + PEDOMETER_WRAP_SECOND = 0x51, -- The second within each RTC hour at which the step history should roll into the next hour. + + VOLUME_CALIBRATION_MIN = 0x58, -- Lower bound of sound volume. + VOLUME_CALIBRATION_MAX = 0x59, -- Upper bound of sound volume. + + STORAGE_AREA_OFFSET = 0x60, -- Offset within the storage area to read/write to. + STORAGE_AREA = 0x61, -- Input/output byte for write/read operations in the storage area. + + INFO = 0x7F, -- System information register. + }, + + -- Bitmasks for the reset event flags + reset_event_flags = { + RTC_TIME_LOST = 1 << 0, -- RTC time was lost (as is the case when the battery is removed). + WATCHDOG_RESET = 1 << 1, -- MCU Watchdog reset occurred. + }, + + -- Bitmasks for the power status + power_status_flags = { + SHELL_OPEN = 1 << 1, -- Set if the shell is open. + ADAPTER_CONNECTED = 1 << 3, -- Set if the AC adapter is connected. + CHARGING = 1 << 4, -- Set if the battery is charging. + BOTTOM_BL_ON = 1 << 5, -- Set if the bottom backlight is on. + TOP_BL_ON = 1 << 6, -- Set if the top backlight is on. + LCD_ON = 1 << 7 -- Set if the LCDs are on. + }, + + -- Bitmasks for the pedometer control + pedometer_control = { + CLEAR = 1 << 0, -- Clear the step history. + STEPS_FULL = 1 << 4 -- Set when the step history is full. + }, + + -- Bitmasks for the power signal triggers + power_trigger = { + SHUTDOWN = 1 << 0, -- Turn off the system. + RESET = 1 << 1, -- Reset the MCU. + REBOOT = 1 << 2, -- Reboot the system. + LGY_SHUTDOWN = 1 << 3, -- Turn off the system. (Used by LgyBg) + SLEEP = 1 << 4, -- Signal to enter sleep mode. + OLDMCU_BL_OFF = 1 << 4, -- (Old MCU_FIRM only) turn the backlights off. + OLDMCU_BL_ON = 1 << 5, -- (Old MCU_FIRM only) turn the backlights on. + OLDMCU_LCD_OFF = 1 << 6, -- (Old MCU_FIRM only) turn the LCDs off. + OLDMCU_LCD_ON = 1 << 7, -- (Old MCU_FIRM only) turn the LCDs on. + }, + + power_led_state = { + NORMAL = 0, -- Fade power LED to blue, while checking for battery percentage. + FADE_BLUE = 1, -- Fade power LED to blue. + SLEEP_MODE = 2, -- The power LED pulses blue slowly, as it does in sleep mode. + OFF = 3, -- Power LED fades off. + RED = 4, -- Power LED instantaneously turns red. + BLUE = 5, -- Power LED instantaneously turns blue. + BLINK_RED = 6, -- Power LED and info (notification) LED blink red, as they do when the battery is critically low. + }, + + -- WiFi mode register values + wifi_mode = { + CTR = 0, -- 3DS WiFi mode. + MP = 1, -- DS[i] WiFi mode ("MP"). + }, + + -- Sensor configuration register values + sensor_config = { + ACCELEROMETER_ENABLE = 1 << 0, -- If set, the accelerometer is enabled. + PEDOMETER_ENABLE = 1 << 1, -- If set, the pedometer is enabled. + }, + + -- Accelerometer scale settings + accelerometer_scale = { + SCALE_2G = 0x0, -- -2G to 2G + SCALE_4G = 0x1, -- -4G to 4G + SCALE_8G = 0x3, -- -8G to 8G + } +} + +return i2c diff --git a/data/preload.lua b/data/preload.lua index 7c78615..0952898 100644 --- a/data/preload.lua +++ b/data/preload.lua @@ -6,3 +6,4 @@ fs = require('fs') util = require('util') io = require('io') sys = require('sys') +i2c = require('i2c') diff --git a/resources/lua-doc.md b/resources/lua-doc.md index 44e7b82..1ce7ef8 100644 --- a/resources/lua-doc.md +++ b/resources/lua-doc.md @@ -1127,3 +1127,315 @@ Determines if the currently executing script was directly run, or was imported b > This is not well tested. * **Returns:** `true` if the current script was imported as a module + +--- + +### `i2c` module + +#### i2c.dev #### +A list of devices for use with `i2c.read` and `i2c.write` + +Lua | Value +-- | -- +POWER | 0 +POWER | 1 +CAMERA2 | 2 +MCU | 3 +CAMERA3 | 4 +LCD1 | 5 +LCD2 | 6 +GYRO3 | 9 +GYRO | 10 +GYRO2 | 11 +DEBUG_PAD | 12 +IR | 13 +EEPROM | 14 +NFC | 15 +QTM | 16 +N3DS_HID | 17 + +#### i2c.mcu #### + +`i2c.mcu` contains registers and bitmasks for registers + +##### i2c.mcu.reg ##### +Contains some registers in the MCU + +Lua | Value | Notes +-- | -- | -- +VERSION_HIGH | 0x0 | Major version of the MCU firmware. +VERSION_LOW | 0x1 | Minor version of the MCU firmware. +RESET_EVENTS | 0x2 | @ref mcu.reset_event_flags +VCOM_TOP | 0x3 | Flicker/VCOM value for the top screen. +VCOM_BOTTOM | 0x4 | Flicker/VCOM value for the bottom screen. +FIRMWARE_UPLOAD_0 | 0x5 | Firmware upload register. +FIRMWARE_UPLOAD_1 | 0x6 | Firmware upload register. +FIRMWARE_UPLOAD_2 | 0x7 | Firmware upload register. +RAW_3D_SLIDER_POSITION | 0x8 | Position of the 3D slider. +VOLUME_SLIDER_POSITION | 0x9 | Position of the volume slider. +BATTERY_PCB_TEMPERATURE | 0xA | Temperature of the battery, measured on a sensor on the PCB. +BATTERY_PERCENTAGE_INT | 0xB | Integer part of the battery percentage. +BATTERY_PERCENTAGE_FRAC | 0xC | Fractional part of the battery percentage. +BATTERY_VOLTAGE | 0xD | Voltage of the battery, in units of 20 mV. +HW_STATUS | 0xE | Hardware status bits +POWER_STATUS | 0xF | @ref mcu.power_status_flags +LEGACY_VERSION_HIGH | 0xF | (Old MCU_FIRM only) Major firmware version. +LEGACY_VERSION_LOW | 0x10 | (Old MCU_FIRM only) Minor firmware version. +LEGACY_FIRM_UPLOAD | 0x3B | (Old MCU_FIRM only) Firmware upload register. +RECEIVED_IRQS_1 | 0x10 | Bitmask of received IRQs. @ref mcu.interrupt_1 +RECEIVED_IRQS_2 | 0x11 | Bitmask of received IRQs. @ref mcu.interrupt_2 +RECEIVED_IRQS_3 | 0x12 | Bitmask of received IRQs. @ref mcu.interrupt_3 +RECEIVED_IRQS_4 | 0x13 | Bitmask of received IRQs. @ref mcu.interrupt_4 +UNUSED_RO_14 | 0x14 | Unused read-only register. +USERDATA_RAM_15 | 0x15 | Unused register, supports writing +USERDATA_RAM_16 | 0x16 | Unused register, supports writing +USERDATA_RAM_17 | 0x17 | Unused register, supports writing +IRQ_MASK_1 | 0x18 | Bitmask of enabled IRQs. @ref mcu.interrupt_1 +IRQ_MASK_2 | 0x19 | Bitmask of enabled IRQs. @ref mcu.interrupt_2 +IRQ_MASK_3 | 0x1A | Bitmask of enabled IRQs. @ref mcu.interrupt_3 +IRQ_MASK_4 | 0x1B | Bitmask of enabled IRQs. @ref mcu.interrupt_4 +USERDATA_RAM_1C | 0x1C | Unused register, supports writing +USERDATA_RAM_1D | 0x1D | Unused register, supports writing +USERDATA_RAM_1E | 0x1E | Unused register, supports writing +USERDATA_RAM_1F | 0x1F | Unused register, supports writing +PWR_CTL | 0x20 | @ref mcu.power_trigger +LCD_PWR_CTL | 0x22 | LCD Power control +MCU_RESET_CTL | 0x23 | Writing 'r' to this register resets the MCU. Stubbed on retail. +FORCE_SHUTDOWN_DELAY | 0x24 | The amount of time, in units of 0.125s, for which the power button needs to be held to trigger a hard shutdown. +VOLUME_UNK_25 | 0x25 | Unknown register. Used in mcu::SND +UNK_26 | 0x26 | Unknown register. Used in mcu::CDC +VOLUME_SLIDER_RAW | 0x27 | Raw value of the volume slider, in the range of 0-255 +LED_BRIGHTNESS_STATE | 0x28 | Brightness of the status LEDs. +POWER_LED_STATE | 0x29 | @ref mcu.power_led_state +WLAN_LED_STATE | 0x2A | Controls the WiFi LED. +CAMERA_LED_STATE | 0x2B | Controls the camera LED (on models that have one). +LED_3D_STATE | 0x2C | Controls the 3D LED (on models that have one). +NOTIFICATION_LED_STATE | 0x2D | Controls the info (notification) LED. +NOTIFICATION_LED_CYCLE_STATE | 0x2E | Bit 0 is set if the info (notification) LED has started a new cycle of its pattern. +RTC_TIME_SECOND | 0x30 | RTC second. +RTC_TIME_MINUTE | 0x31 | RTC minute. +RTC_TIME_HOUR | 0x32 | RTC hour. +RTC_TIME_WEEKDAY | 0x33 | RTC day of the week. +RTC_TIME_DAY | 0x34 | RTC day of the month. +RTC_TIME_MONTH | 0x35 | RTC month. +RTC_TIME_YEAR | 0x36 | RTC year. +RTC_TIME_CORRECTION | 0x37 | RTC subsecond (RSUBC) correction value. +RTC_ALARM_MINUTE | 0x38 | RTC alarm minute. +RTC_ALARM_HOUR | 0x39 | RTC alarm hour. +RTC_ALARM_DAY | 0x3A | RTC alarm day. +RTC_ALARM_MONTH | 0x3B | RTC alarm month. +RTC_ALARM_YEAR | 0x3C | RTC alarm year. +TICK_COUNTER_LSB | 0x3D | MCU tick counter value (low byte). +TICK_COUNTER_MSB | 0x3E | MCU tick counter value (high byte). +UNK_3F | 0x3F | Unknown register +SENSOR_CONFIG | 0x40 | @ref mcu.sensor_config +ACCELEROMETER_MANUAL_REGID_R | 0x41 | Hardware register ID for use in manual accelerometer I²C reads. +ACCELEROMETER_MANUAL_REGID_W | 0x43 | Hardware reigster ID for use in manual accelerometer I²C writes. +ACCELEROMETER_MANUAL_IO | 0x44 | Data register for manual accelerometer reads/writes. +ACCELEROMETER_OUTPUT_X_LSB | 0x45 | Accelerometer X coordinate (low byte). +ACCELEROMETER_OUTPUT_X_MSB | 0x46 | Accelerometer X coordinate (high byte). +ACCELEROMETER_OUTPUT_Y_LSB | 0x47 | Accelerometer Y coordinate (low byte). +ACCELEROMETER_OUTPUT_Y_MSB | 0x48 | Accelerometer Y coordinate (high byte). +ACCELEROMETER_OUTPUT_Z_LSB | 0x49 | Accelerometer Z coordinate (low byte). +ACCELEROMETER_OUTPUT_Z_MSB | 0x4A | Accelerometer Z coordinate (high byte). +PEDOMETER_STEPS_LOWBYTE | 0x4B | Pedometer step count (low byte). +PEDOMETER_STEPS_MIDDLEBYTE | 0x4C | Pedometer step count (middle byte). +PEDOMETER_STEPS_HIGHBYTE | 0x4D | Pedometer step count (high byte). +PEDOMETER_CNT | 0x4E | @ref mcu.pedometer_control +PEDOMETER_STEP_DATA | 0x4F | Step history and time of last update. +PEDOMETER_WRAP_MINUTE | 0x50 | The minute within each RTC hour at which the step history should roll into the next hour. +PEDOMETER_WRAP_SECOND | 0x51 | The second within each RTC hour at which the step history should roll into the next hour. +VOLUME_CALIBRATION_MIN | 0x58 | Lower bound of sound volume. +VOLUME_CALIBRATION_MAX | 0x59 | Upper bound of sound volume. +STORAGE_AREA_OFFSET | 0x60 | Offset within the storage area to read/write to. +STORAGE_AREA | 0x61 | Input/output byte for write/read operations in the storage area. +INFO | 0x7F | System information register. + +##### i2c.mcu.interrupt_1 ##### +Bitmask for interrupts (1) + +Lua | Bit | Notes +-- | -- | -- +POWER_BUTTON_PRESS | 0 | The power button was pressed +POWER_BUTTON_HELD | 1 | The power button was held +HOME_BUTTON_PRESS | 2 | The HOME button was pressed +HOME_BUTTON_RELEASE | 3 | The HOME button was released +WLAN_SWITCH_TRIGGER | 4 | The WiFi switch was triggered +SHELL_CLOSE | 5 | The shell was closed (or sleep switch turned off) +SHELL_OPEN | 6 | The shell was opened (or sleep switch turned on) +FATAL_HW_ERROR | 7 | MCU watchdog reset occurred + +##### i2c.mcu.interrupt_2 ##### +Bitmask for interrupts (2) + +Lua | Bit | Notes +-- | -- | -- +AC_ADAPTER_REMOVED | 0 | The AC adapter was disconnected +AC_ADAPTER_PLUGGED_IN | 1 | The AC adapter was connected +RTC_ALARM | 2 | The RTC alarm time has been reached +ACCELEROMETER_I2C_MANUAL_IO | 3 | The manual accelerometer I²C read/write operation has completed +ACCELEROMETER_NEW_SAMPLE | 4 | A new accelerometer sample is available +CRITICAL_BATTERY | 5 | The battery is critically low +CHARGING_STOP | 6 | The battery stopped charging +CHARGING_START | 7 | The battery started charging + + +##### i2c.mcu.interrupt_3 ##### +Bitmask for interrupts (3) + +Lua | Bit | Notes +-- | -- | -- +VOL_SLIDER | 6 | The position of the volume slider changed + + +##### i2c.mcu.interrupt_4 ##### +Bitmask for interrupts (4) + +Lua | Bit | Notes +-- | -- | -- +LCD_OFF | 0 | The LCDs turned off. +LCD_ON | 1 | The LCDs turned on. +BOT_BACKLIGHT_OFF | 2 | The bottom screen backlight turned off. +BOT_BACKLIGHT_ON | 3 | The bottom screen backlight turned on. +TOP_BACKLIGHT_OFF | 4 | The top screen backlight turned off. +TOP_BACKLIGHT_ON | 5 | The top screen backlight turned on. + +##### i2c.mcu.reset_event_flags ##### +Bitmasks for the reset event flags +Lua | Bit | Notes +-- | -- | -- +RTC_TIME_LOST | 0 | RTC time was lost (as is the case when the battery is removed). +WATCHDOG_RESET | 1 | MCU Watchdog reset occurred. + +##### i2c.mcu.power_status_flags ##### +Bitmasks for the power status +Lua | Bit | Notes +-- | -- | -- +SHELL_OPEN | 1 | Set if the shell is open. +ADAPTER_CONNECTED | 3 | Set if the AC adapter is connected. +CHARGING | 4 | Set if the battery is charging. +BOTTOM_BL_ON | 5 | Set if the bottom backlight is on. +TOP_BL_ON | 6 | Set if the top backlight is on. +LCD_ON | 7 | Set if the LCDs are on. + +##### i2c.mcu.pedometer_control ##### +Bitmasks for the pedometer control +Lua | Bit | Notes +-- | -- | -- +CLEAR | 0 | Clear the step history. +STEPS_FULL | 4 | Set when the step history is full. + +##### i2c.mcu.power_trigger ##### +Bitmasks for the power signal triggers +Lua | Bit | Notes +-- | -- | -- +SHUTDOWN | 0 | Turn off the system. +RESET | 1 | Reset the MCU. +REBOOT | 2 | Reboot the system. +LGY_SHUTDOWN | 3 | Turn off the system. (Used by LgyBg) +SLEEP | 4 | Signal to enter sleep mode. +OLDMCU_BL_OFF | 4 | (Old MCU_FIRM only) turn the backlights off. +OLDMCU_BL_ON | 5 | (Old MCU_FIRM only) turn the backlights on. +OLDMCU_LCD_OFF | 6 | (Old MCU_FIRM only) turn the LCDs off. +OLDMCU_LCD_ON | 7 | (Old MCU_FIRM only) turn the LCDs on. + +##### i2c.mcu.power_led_state ##### +Lua | Value | Notes +-- | -- | -- +NORMAL | 0 | Fade power LED to blue, while checking for battery percentage. +FADE_BLUE | 1 | Fade power LED to blue. +SLEEP_MODE | 2 | The power LED pulses blue slowly, as it does in sleep mode. +OFF | 3 | Power LED fades off. +RED | 4 | Power LED instantaneously turns red. +BLUE | 5 | Power LED instantaneously turns blue. +BLINK_RED | 6 | Power LED and info (notification) LED blink red, as they do when the battery is critically low. + +##### i2c.mcu.wifi_mode ##### +WiFi mode register values +Lua | Value | Notes +-- | -- | -- +CTR | 0 | 3DS WiFi mode. +MP | 1 | DS[i] WiFi mode ("MP"). + +##### i2c.mcu.sensor_config ##### +Sensor configuration register values +Lua | Bit | Notes +-- | -- | -- +ACCELEROMETER_ENABLE | 0 | If set, the accelerometer is enabled. +PEDOMETER_ENABLE | 1 | If set, the pedometer is enabled. + +##### i2c.mcu.accelerometer_scale ##### +Accelerometer scale settings +Lua | Value | Notes +-- | -- | -- +SCALE_2G | 0x0 | -2G to 2G +SCALE_4G | 0x1 | -4G to 4G +SCALE_8G | 0x3 | -8G to 8G + + +#### i2c.read + +* `table i2c.read(int device_id, int register_address, int length)` + +Read data from an I²C device register. Returns the data as a table of integers (0-255) for direct math operations. + +* **Arguments** + * `device_id` - I²C device ID (0-17). + * `register_address` - Register address to read from (0-255) + * `length` - Number of bytes to read (1-64) +* **Returns:** Table of byte values as integers +* **Throws** + * `"Invalid device ID: ## (must be 0-17)"` - device ID is outside valid range + * `"Invalid register address: ## (must be 0-255)"` - register address is outside valid range + * `"Invalid length: ## (must be 1-64)"` - length is outside valid range + * `"I2C read failed"` - hardware I²C operation failed + +Example: +```lua +-- Read voltage from MCU device +local voltage_raw = i2c.read(i2c.dev.MCU, i2c.mcu.reg.BATTERY_VOLTAGE, 1) +local voltage = voltage_raw[1] * 5 / 256.0 +print("System voltage:", voltage, "V") + +-- Read Power status +local power_state = i2c.read(i2c.dev.MCU, i2c.mcu.reg.POWER_STATUS, 1) +if power_state[1] & i2c.mcu.power_status_flags.SHELL_OPEN ~= 0 then + print("Shell is open") +end +if power_state[1] & i2c.mcu.power_status_flags.CHARGING ~= 0 then + print("Charging") +end +``` + +#### i2c.write + +* `void i2c.write(int device_id, int register_address, table data)` + +> [!WARNING] +> Writing data to random registers can lead to bricking!. + +Write data to an I²C device register. Requires memory write permissions. +Not all registers are supported for writing. + +* **Allowed registers (MCU)** + * `mcu.reg.USERDATA_RAM_` - Registers 0x15-0x17 and 0x1C-0x1F + * `mcu.reg.LED_BRIGHTNESS_STATE` - Brightness of the status LEDs. + * `mcu.reg.POWER_LED_STATE` - Sets the state of the power LED + * `mcu.reg.WLAN_LED_STATE` - Sets the state of the wifi LED + * `mcu.reg.CAMERA_LED_STATE` - Sets the state of the camera LED + * `mcu.reg.LED_3D_STATE` - Sets the state of the 3D LED + * `mcu.reg.NOTIFICATION_LED_STATE` - Sets the state of the info (notification) LED +* **Arguments** + * `device_id` - I²C device ID (0-17). + * `register_address` - Register address to write to (0-255) + * `data` - Table of byte values to write (each value must be 0-255) +* **Throws** + * `"Invalid device ID: ## (must be 0-17)"` - device ID is outside valid range + * `"Invalid register address: ## (must be 0-255)"` - register address is outside valid range + * `"Write to device ##, register ## is not allowed"` - The device/register is not on the allowed write list + * `"Third parameter must be a table of byte values"` - data parameter is not a table + * `"Invalid data length: ## (must be 1-64)"` - data table length is outside valid range + * `"Table element ## is not an integer"` - data table contains non-integer values + * `"Table element ## is out of range: ## (must be 0-255)"` - data table contains values outside byte range + * `"I2C write failed"` - hardware I²C operation failed + * Permission errors if memory write access is denied