tools: mtd: add mram_io byte-wise read/write tool for MTD_RAM devices

mram_io is a small user-space utility that maps a MTD_RAM device
(e.g. Everspin MRAM on Cadence OSPI in DAC mode) into its address
space via mmap() and performs byte-granular reads or writes without
any kernel copy overhead.

Usage:
  mram_io --device <device> --read --offset <off> --length <n>
  mram_io --device <device> --write --offset <off> --data <b0> [b1 b2 ...]

Options:
  --device / -D <n>  MTD character device, e.g. /dev/mtd0
  --offset / -o <n>  Byte offset (hex or decimal)
  --length / -l <n>  Number of bytes to read
  --data   / -d      Remaining arguments are bytes to write

Read output is a classic hex-dump with offset, hex columns (split
8+8) and an ASCII column.

Write mode logs each byte as it is written, then performs an
immediate read-back and reports OK / MISMATCH per byte.

Bytes are written as volatile stores to prevent the compiler from
optimising away individual byte accesses; the mmap region is mapped
noncached by the kernel (pgprot_noncached) so every store goes
straight to the hardware.

Argument parsing uses getopt_long and utilizes the device path as a
named option (--device / -D) to eliminate positional ambiguity when
passing data bytes.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
This commit is contained in:
Heinrich Toews
2026-04-23 12:26:09 +02:00
parent 26cea97562
commit e9ad8bc142
2 changed files with 367 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
# SPDX-License-Identifier: GPL-2.0-only
include ../scripts/Makefile.include
bindir ?= /usr/sbin
ifeq ($(srctree),)
srctree := $(patsubst %/,%,$(dir $(CURDIR)))
srctree := $(patsubst %/,%,$(dir $(srctree)))
endif
# Do not use make's built-in rules
# (this improves performance and avoids hard-to-debug behaviour);
MAKEFLAGS += -r
CFLAGS += -O2 -Wall -Wextra -g -D_GNU_SOURCE
ALL_TARGETS := mram_io
ALL_PROGRAMS := $(patsubst %,$(OUTPUT)%,$(ALL_TARGETS))
all: $(ALL_PROGRAMS)
export srctree OUTPUT CC LD CFLAGS
include $(srctree)/tools/build/Makefile.include
$(OUTPUT)mram_io: mram_io.c
$(CC) $(CFLAGS) -o $@ $<
clean:
$(RM) $(ALL_PROGRAMS)
install: all
install -d $(DESTDIR)$(bindir)
install -m 755 $(ALL_PROGRAMS) $(DESTDIR)$(bindir)
+334
View File
@@ -0,0 +1,334 @@
// SPDX-License-Identifier: GPL-2.0
/*
* mram_io - Byte-wise read/write tool for MTD_RAM devices via mmap()
*
* Requires a MTD device that supports mmap() (e.g. Everspin MRAM on
* Cadence OSPI in DAC mode, registered as MTD_RAM).
*
* Usage:
* mram_io --read --device <dev> --offset <offset> --length <n>
* mram_io --write --device <dev> --offset <offset> --data <b0> [b1 b2 ...]
*
* Examples:
* mram_io --read --device /dev/mtd0 --offset 0x100 --length 16
* mram_io --write --device /dev/mtd0 --offset 0x100 --data 0xde 0xad 0xbe 0xef
*/
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
static void usage(const char *prog)
{
fprintf(stderr,
"Usage:\n"
" %s --read --device <dev> --offset <off> --length <n>\n"
" %s --write --device <dev> --offset <off> --data <b0> [b1 ...]\n"
"\n"
"Options:\n"
" --device, -D <path> MTD character device, e.g. /dev/mtd0\n"
" --offset, -o <n> Byte offset into the device (hex or dec)\n"
" --length, -l <n> Number of bytes to read\n"
" --data, -d Following args are bytes to write (hex or dec)\n"
" --read, -r Read mode\n"
" --write, -w Write mode\n"
" --help, -h Show this help\n"
"\n"
"Output (read): hex-dump with offset, hex columns and ASCII column.\n",
prog, prog);
}
static unsigned long parse_num(const char *s)
{
char *end;
unsigned long v = strtoul(s, &end, 0);
if (*end != '\0') {
fprintf(stderr, "Invalid number: '%s'\n", s);
exit(EXIT_FAILURE);
}
return v;
}
/* Returns the size of the MTD device via lseek(). */
static off_t mtd_size(int fd)
{
off_t sz = lseek(fd, 0, SEEK_END);
if (sz < 0) {
perror("lseek (SEEK_END)");
exit(EXIT_FAILURE);
}
lseek(fd, 0, SEEK_SET);
return sz;
}
/* ------------------------------------------------------------------ */
/* main */
/* ------------------------------------------------------------------ */
enum mode { MODE_NONE, MODE_READ, MODE_WRITE };
int main(int argc, char *argv[])
{
static const struct option long_opts[] = {
{ "read", no_argument, NULL, 'r' },
{ "write", no_argument, NULL, 'w' },
{ "device", required_argument, NULL, 'D' },
{ "offset", required_argument, NULL, 'o' },
{ "length", required_argument, NULL, 'l' },
{ "data", no_argument, NULL, 'd' },
{ "help", no_argument, NULL, 'h' },
{ NULL, 0, NULL, 0 },
};
enum mode mode = MODE_NONE;
const char *device = NULL;
unsigned long offset = 0;
unsigned long length = 0;
int have_offset = 0;
int have_length = 0;
int data_mode = 0;
int opt;
int opt_idx = 0;
/*
* Parse all named options first. Stop at --data/-d: everything
* after it is a raw byte value for --write, not an option.
*/
while (!data_mode &&
(opt = getopt_long(argc, argv, "rwD:o:l:dh",
long_opts, &opt_idx)) != -1) {
switch (opt) {
case 'r':
mode = MODE_READ;
break;
case 'w':
mode = MODE_WRITE;
break;
case 'D':
device = optarg;
break;
case 'o':
offset = parse_num(optarg);
have_offset = 1;
break;
case 'l':
length = parse_num(optarg);
have_length = 1;
break;
case 'd':
data_mode = 1;
break;
case 'h':
usage(argv[0]);
return EXIT_SUCCESS;
default:
usage(argv[0]);
return EXIT_FAILURE;
}
}
/* --- Validate ------------------------------------------------- */
if (!device) {
fprintf(stderr, "Error: --device <path> is required.\n\n");
usage(argv[0]);
return EXIT_FAILURE;
}
if (mode == MODE_NONE) {
fprintf(stderr, "Error: specify --read or --write.\n\n");
usage(argv[0]);
return EXIT_FAILURE;
}
if (!have_offset) {
fprintf(stderr, "Error: --offset is required.\n\n");
usage(argv[0]);
return EXIT_FAILURE;
}
/* Collect write bytes from remaining argv (after --data) */
uint8_t *write_buf = NULL;
size_t write_cnt = 0;
if (mode == MODE_WRITE) {
if (!data_mode) {
fprintf(stderr,
"Error: --write requires --data <b0> [b1 ...].\n\n");
usage(argv[0]);
return EXIT_FAILURE;
}
write_cnt = (size_t)(argc - optind);
if (write_cnt == 0) {
fprintf(stderr, "Error: no data bytes given after --data.\n\n");
usage(argv[0]);
return EXIT_FAILURE;
}
write_buf = malloc(write_cnt);
if (!write_buf) {
perror("malloc");
return EXIT_FAILURE;
}
for (size_t i = 0; i < write_cnt; i++) {
unsigned long v = parse_num(argv[optind + i]);
if (v > 0xff) {
fprintf(stderr,
"Error: byte value 0x%lx out of range.\n", v);
free(write_buf);
return EXIT_FAILURE;
}
write_buf[i] = (uint8_t)v;
}
length = write_cnt;
}
if (mode == MODE_READ && !have_length) {
fprintf(stderr, "Error: --read requires --length.\n\n");
usage(argv[0]);
return EXIT_FAILURE;
}
/* --- Open device ---------------------------------------------- */
int flags = (mode == MODE_WRITE) ? (O_RDWR | O_SYNC) : O_RDONLY;
int fd = open(device, flags);
if (fd < 0) {
fprintf(stderr, "open(%s): %s\n", device, strerror(errno));
free(write_buf);
return EXIT_FAILURE;
}
off_t dev_size = mtd_size(fd);
if ((unsigned long)dev_size == 0) {
fprintf(stderr, "Error: device size is 0.\n");
goto err_close;
}
if (offset >= (unsigned long)dev_size) {
fprintf(stderr,
"Error: offset 0x%lx is beyond device size 0x%lx.\n",
offset, (unsigned long)dev_size);
goto err_close;
}
if (offset + length > (unsigned long)dev_size) {
fprintf(stderr,
"Warning: clamping length from %lu to %lu (end of device).\n",
length, (unsigned long)dev_size - offset);
length = (unsigned long)dev_size - offset;
}
/* --- mmap ----------------------------------------------------- */
/*
* mmap() the whole device. The kernel (mtdchar_mmap) maps the
* physical AHB window directly into our address space; accesses
* go straight to the MRAM without any kernel copy.
*/
int prot = PROT_READ | ((mode == MODE_WRITE) ? PROT_WRITE : 0);
void *map = mmap(NULL, (size_t)dev_size, prot, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
fprintf(stderr, "mmap(%s): %s\n", device, strerror(errno));
fprintf(stderr,
"Note: device must be of type MTD_RAM with a physical "
"AHB window (e.g. Everspin MRAM on Cadence OSPI).\n");
goto err_close;
}
uint8_t *base = (uint8_t *)map + offset;
/* --- Read ----------------------------------------------------- */
if (mode == MODE_READ) {
printf("Reading %lu byte(s) from %s at offset 0x%lx:\n\n",
length, device, offset);
printf(" Offset 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f"
" ASCII\n");
printf(" -------- -----------------------------------------------"
" ----------------\n");
for (unsigned long i = 0; i < length; ) {
unsigned long row_off = offset + i;
unsigned long row_start = i;
/* Offset column */
printf(" %08lx ", row_off);
/* Hex bytes: 16 per row, split 8+8 */
for (int col = 0; col < 16; col++) {
if (col == 8)
printf(" ");
if (row_start + col < length)
printf("%02x ", base[row_start + col]);
else
printf(" ");
}
/* ASCII column */
printf(" |");
for (int col = 0; col < 16 && row_start + col < length; col++) {
uint8_t c = base[row_start + col];
printf("%c", (c >= 0x20 && c < 0x7f) ? c : '.');
}
printf("|\n");
i += 16;
}
printf("\n");
}
/* --- Write ---------------------------------------------------- */
if (mode == MODE_WRITE) {
printf("Writing %lu byte(s) to %s at offset 0x%lx:\n",
length, device, offset);
for (size_t i = 0; i < write_cnt; i++) {
/*
* Volatile write: ensure each byte reaches the hardware
* window. The mmap region is pgprot_noncached, but being
* explicit prevents compiler from merging byte stores.
*/
volatile uint8_t *dst = (volatile uint8_t *)base + i;
*dst = write_buf[i];
printf(" [0x%08lx] <- 0x%02x\n",
offset + i, write_buf[i]);
}
/* Read back for verification */
printf("\nVerification (read-back):\n");
for (size_t i = 0; i < write_cnt; i++) {
uint8_t got = *((volatile uint8_t *)base + i);
const char *status = (got == write_buf[i]) ? "OK" : "MISMATCH";
printf(" [0x%08lx] wrote 0x%02x read 0x%02x %s\n",
offset + i, write_buf[i], got, status);
}
}
munmap(map, (size_t)dev_size);
free(write_buf);
close(fd);
return EXIT_SUCCESS;
err_close:
free(write_buf);
close(fd);
return EXIT_FAILURE;
}