Address-aware helpers (*_at) take absolute MCU addresses;
the buffer offset is computed automatically:
buffer_offset = addr - region.start_address.
-- Flash region starts at 0x4000. Read 4 bytes at MCU address 0xF650:
local bytes = mp.memory.read_at("Flash", 0xF650, 4)
-- internally reads buffer offset: 0xF650 - 0x4000 = 0xB650
Region navigation
| Function | Returns | Description |
|---|
get_regions() | table | Array of region names |
current_region() | string | Name of active region |
region_count() | int | Number of regions |
select_region(idx) | void | Switch region by 0-based index |
select_region_by_name(n) | bool | Switch region by name |
get_region_info([r]) | table | {index, name, size, start_address, end_address, cell_size} — r is name (string), 0-based index, or omitted (active) |
region_size(r) | int | Region size in bytes — r is name or 0-based index |
Writability
Tabs open in read-only by default, just like the GUI. Writes silently
return false until you enable writability.
| Function | Returns | Description |
|---|
set_writable(r, b) | ok, err | r is name (string) or 0-based index (int); b is bool. Bare set_writable(b) (single bool) toggles all tabs. A bare integer set_writable(0) returns (false, "...") |
set_writable_all(b) | ok, err | Apply to all regions |
is_writable([r]) | bool | r is name, 0-based index, or omitted (active region) |
Reads — offset-based
| Function | Returns | Description |
|---|
read_byte(r, off) | int | Single byte (-1 if out of range) |
read_range(r, start, len) | table | Byte array |
read_bytes(r, off, n) | table | Byte array (alias-class with read_range) |
read_hex(r, off, n) | string | Hex string e.g. "deadbeef" |
read_string(r, off, maxLen) | string | Raw string (may include non-printable) |
read_string_until_null(r, off, maxLen?) | string | Read up to first NUL byte (max 256 by default) |
get_region_data(r) | table | Entire region as byte array |
get_all_data() | table | All regions concatenated as byte array |
Reads — address-based
| Function | Returns | Description |
|---|
read_at(r, addr, n) | table | Byte array starting at MCU address |
read_byte_at(r, addr) | int | Single byte at MCU address |
read_string_at(r, addr, maxLen) | string | Raw string at MCU address |
read_string_until_null_at(r, addr, maxLen?) | string | Read up to first NUL at absolute address (max 256) |
Writes — offset-based
| Function | Returns | Description |
|---|
write_byte(r, off, v) | bool | Single byte |
write_range(r, off, bytes) | bool | Byte array |
write_hex(r, off, hexStr) | bool | Bytes from hex string |
write_string(r, off, text) | bool | Raw string |
fill(r, off, len, v) | bool | Fill with constant byte |
fill_pattern(r, off, len, pattern) | bool | Fill with repeating byte pattern |
Writes — address-based
| Function | Returns | Description |
|---|
write_at(r, addr, bytes) | bool | Byte array at MCU address |
write_byte_at(r, addr, v) | bool | Single byte at MCU address |
write_string_at(r, addr, text) | bool | Raw string at MCU address |
Search
| Function | Returns | Description |
|---|
find(r, hex, startOff?) | int | First match offset, or -1 |
find_prev(r, hex, startOff?) | int | Last match before start, or -1 |
find_all(r, hex) / find_all(r, byte_table) | table | Array of offsets within the region. For ASCII needles use the byte-table form or find_string |
find_string(r, text, startOff?) | int | First ASCII match offset, or -1 |
search_all(hex) / search_all(region, hex) | table | {region, offset, address, offset_hex, address_hex} per hit. No-region form scans every region |
search_string_all(text) / search_string_all(region, text) | table | Same plus context_hex (matched bytes + 16 trailing) |
search_patterns(patterns) | table | Batch: {label, hits[], count} |
Selection & cursor
| Function | Returns | Description |
|---|
set_selection(r, off, len) | void | Highlight bytes |
get_selection(r) | table | {start, end, length, hex} or empty |
go_to(r, off) | ok, err | Move cursor to offset |
cursor_offset(r) | int | Current cursor offset |
Undo / redo
| Function | Returns | Description |
|---|
undo(r) | void | Undo last edit |
redo(r) | void | Redo |
can_undo(r) | bool | — |
can_redo(r) | bool | — |
Bookmarks
| Function | Returns | Description |
|---|
add_bookmark(r, off, comment) / add_bookmark(off, comment) | ok, err | Add a bookmark |
get_bookmarks(r) / get_bookmarks() / list_bookmarks(...) | table | Array of {offset, comment} |
clear_bookmarks(r) | void | Remove all |
Address conversion
| Function | Returns | Description |
|---|
addr_to_offset(r, addr) | int | MCU address → buffer offset (-1 if below base) |
offset_to_addr(r, off) | int | Buffer offset → MCU address |
Buffer & export
| Function | Returns | Description |
|---|
clear() | void | Clear all buffers (via backend) |
load_file() / show_load_dialog() | void | Open Import file dialog |
load_file(path) | ok, err | Parse file, populate matching regions by address |
save_file() / show_save_dialog() | void | Open Export file dialog |
save_file(path) | ok, err | Write all regions to <path> (format from extension) |
export_region(r, path[, off, len]) | string | Error or empty. Optional off, len carve a buffer-byte slice; output's first record uses start_address + off |
export_all(path) | string | Export all regions to single file |
compare(r1, r2) | table | {diffs[], diff_count, size1, size2, equal} |
Region-name and cell_size pitfalls
Three things that catch every new script author:
1. Region names are not "Flash" everywhere
Each backend uses its own names — P_Flash, D_Flash, RAM_P (DSC),
Code Flash, EEPROM, User Boot, etc. Don't hardcode. Enumerate
first:
for i, name in ipairs(mp.memory.get_regions()) do
mp.log.info(i .. ": " .. name)
end
A wrong name makes read_byte / go_to / cursor_offset silently
return 0 / false / -1. The backend logs a [QT-WARN] listing
the names it actually has — check the log when something "always
returns 0".
2. Buffer initial content depends on the backend
Most backends pre-fill each region with 0xFF at full size on
set_target, so region_size may already return the real flash
size and read_byte returns 0xFF — that's NOT the chip's content,
it's a dummy erased-flash fill. Always call mp.backend.read() (or
mp.file.load(path)) before assuming mp.memory.read_* reflects
what's on the chip.
3. Word-addressed targets — DSC, dsPIC and friends
Freescale / NXP DSC (MC56F8xxx) is word-addressed: one device
address points to a 16-bit cell, but the buffer holds raw bytes,
so region.size is 2× the addressable word count and
cell_size = 2.
Check cell_size and convert when crossing the address ↔ buffer-offset
boundary:
local info = mp.memory.get_region_info() -- e.g. P_Flash on MC56F8006
-- info.start_address = 0x800, size = 12288 (bytes), cell_size = 2
-- → 6144 device words from word-address 0x800 to 0x37FF
-- byte_offset (Lua API) = (device_address - start_address) * cell_size
local addr = 0x900
local byte_offset = (addr - info.start_address) * info.cell_size -- 0x200
local lo = mp.memory.read_byte(info.name, byte_offset)
local hi = mp.memory.read_byte(info.name, byte_offset + 1)
local word = lo | (hi << 8)
The _at helpers (read_at, read_byte_at, write_at,
write_byte_at, addr_to_offset, offset_to_addr) do NOT
multiply by cell_size — they treat address - start_address
as a byte offset into the buffer. On cell_size == 1 that's
identity; on DSC you must pre-multiply by cell_size yourself,
or work in byte offsets directly with read_range / read_byte.
mp.memory.go_to(region, off) and cursor_offset(region) operate
on buffer bytes, not device words — multiply your address by
cell_size if it came from a datasheet or HEX file.