am335xussetpm user space device driver

In this thread I hope to document implementing a Beaglebone Black device driver to write the pinmux registers from user space. I’m new to this platform so I’m sure there are some issues to resolve. The driver seems to work after minimal testing. Please beat it up and see if it works for you, First the beagle-version

sudo beagle-version
eeprom:[A335BNLTEID02547SBI04226]
model:[TI_AM335x_BeagleBone_Black]
dogtag:[BeagleBoard.org Debian Trixie Base Image 2026-03-17]
bootloader:[microSD-(push-button)]:[/dev/mmcblk0]:[U-Boot SPL 2022.04-g5509547b (Jan 22 2026 - 19:56:08 +0000)]:[location: dd MBR]
bootloader:[eMMC-(default)]:[/dev/mmcblk1]:[U-Boot SPL 2022.04-gc6f4cf7d (Apr 24 2025 - 03:22:59 +0000)]:[location: dd MBR]
UBOOT: Booted Device-Tree:[am335x-boneblack-uboot.dts]
UBOOT: Loaded Overlay:[BB-ADC-00A0.kernel]
UBOOT: Loaded Overlay:[BB-BONE-eMMC1-01-00A0.kernel]
UBOOT: Loaded Overlay:[BB-UART2-00A0.kernel]
kernel:[6.19.12-00008-g7c1a867566d6-dirty]
/boot/uEnv.txt Settings:
uboot_overlay_options:[enable_uboot_overlays=1]
uboot_overlay_options:[uboot_overlay_addr4=BB-UART2-00A0.dtbo]
uboot_overlay_options:[disable_uboot_overlay_video=1]
uboot_overlay_options:[disable_uboot_overlay_audio=1]
pkg check: to individually upgrade run: [sudo apt install --only-upgrade <pkg>]
pkg:[bb-customizations]:[1.20250808.0-0~trixie+20250808]
pkg:[bb-usb-gadgets]:[1.20250523.1-0~trixie+20250527]
pkg:[bb-wl18xx-firmware]:[1.20230703.0-0~trixie+20240703]
pkg:[kmod]:[34.2-2bbbio1~trixie+20250522]
groups:[debian : debian adm kmem dialout cdrom floppy audio dip video plugdev users systemd-journal input render netdev i2c bluetooth gpio admin tisdk weston-launch]
cmdline:[console=ttyS0,115200n8 root=/dev/mmcblk0p3 ro rootfstype=ext4 rootwait fsck.repair=yes earlycon coherent_pool=1M net.ifnames=0 lpj=1990656 rng_core.default_quality=100]
dmesg | grep remote
[    6.152510] systemd[1]: Reached target remote-fs.target - Remote File Systems.
[   20.345774] remoteproc remoteproc0: wkup_m3 is available
[   22.252233] remoteproc remoteproc0: powering up wkup_m3
[   22.414071] remoteproc remoteproc0: Booting fw image am335x-pm-firmware.elf, size 217148
[   22.422666] remoteproc remoteproc0: remote processor wkup_m3 is now up
dmesg | grep pru
dmesg | grep pinctrl-single
[    0.330316] pinctrl-single 44e10800.pinmux: 142 pins, size 568
dmesg | grep gpio-of-helper
dmesg | grep wlcore
lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 0bda:c811 Realtek Semiconductor Corp. 802.11ac NIC
END

kernel:[6.19.12-00008-g7c1a867566d6-dirty] She’s a dirt girl

Here is the kernel device driver in linux-stable-rcn-ee/drivers/gpio/am335xussetpm.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/mm.h>
#include <linux/io.h>

#define DEVICE_NAME "am335xussetpm"
#define MEM_START   0x44E10800 /* allowed start memory*/
#define MEM_END     0x44E109B4 /* allowed end memory*/
#define MEM_SIZE    (MEM_END - MEM_START + 1)

static int major;
static void __iomem *mem_base;

/* Open/Release */
static int am335xussetpm_open(struct inode *inode, struct file *file) { return 0; }
static int am335xussetpm_release(struct inode *inode, struct file *file) { return 0; }

/* Read: only within allowed range */
static ssize_t am335xussetpm_read(struct file *file, char __user *buf,
                                  size_t count, loff_t *ppos)
{
    if (*ppos >= MEM_SIZE)
        return 0;

    if (count + *ppos > MEM_SIZE)
        count = MEM_SIZE - *ppos;

    if (copy_to_user(buf, mem_base + *ppos, count))
        return -EFAULT;

    *ppos += count;
    return count;
}

/* Write: is restrict to allowed memory range */
static ssize_t am335xussetpm_write(struct file *file, const char __user *buf,
                                   size_t count, loff_t *ppos)
{
    if (*ppos >= MEM_SIZE)
        return -ENOSPC;

    if (count + *ppos > MEM_SIZE)
        count = MEM_SIZE - *ppos;

    if (copy_from_user(mem_base + *ppos, buf, count))
        return -EFAULT;

    *ppos += count;
    return count;
}

/* mmap: map a full page  */
static int am335xussetpm_mmap(struct file *file, struct vm_area_struct *vma)
{
    unsigned long page_start = MEM_START & PAGE_MASK;
    unsigned long size = PAGE_ALIGN(MEM_SIZE); /* map full page */

    if (remap_pfn_range(vma,
                        vma->vm_start,
                        page_start >> PAGE_SHIFT,
                        size,
                        vma->vm_page_prot))
    {
        return -EAGAIN;
    }

    return 0;
}

static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = am335xussetpm_open,
    .release = am335xussetpm_release,
    .read    = am335xussetpm_read,
    .write   = am335xussetpm_write,
    .mmap    = am335xussetpm_mmap,
};

/* Built-in initialization */
static int __init am335xussetpm_init(void)
{
    pr_info("am335xussetpm: Initializing user-space set pinmux driver\n");

    major = register_chrdev(0, DEVICE_NAME, &fops);
    if (major < 0) {
        pr_err("am335xussetpm: failed to register char device\n");
        return major;
    }

    /* Map the physical memory */
    mem_base = ioremap(MEM_START, MEM_SIZE);
    if (!mem_base) {
        unregister_chrdev(major, DEVICE_NAME);
        pr_err("am335xussetpm: ioremap failed\n");
        return -ENOMEM;
    }

    pr_info("am335xussetpm: registered char device major=%d, memory 0x%lx-0x%lx\n",
            major, (unsigned long)MEM_START, (unsigned long)MEM_END);

    return 0;
}

device_initcall(am335xussetpm_init);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("JTBrooks");
MODULE_DESCRIPTION("am335xussetpm: memory-mapped device driver (restricted range)");

and here is the modifications to Makefile and Kconfig

Makefile edit
obj-$(CONFIG_AM335X_SETPINMUX)	+= am335xsetpinmux.o

Kconfig edit
config AM335X_USERSETPINMUX
	bool "AM335X_USERSETPINMUX GPIO PinMux User Space Config"
	default y
	help
	  Say Y here to configure and install AM335X_USERSETPINMUX



Once the kernel is compiled and running

dmesg | grep am335| grep major
[    0.137766] am335xussetpm: registered char device major=249, memory 0x44e10800-0x44e109b4

bash script in rc.local to get the /dev/am335xussetpm to show up. There must be an automatic way to get this to show up in /dev/? Please comment.

# Find the major number from dmesg
major=$(dmesg | grep 'am335xussetpm: registered char device major=' | grep -oP 'major=\K[0-9]+')

if [ -z "$major" ]; then
    echo "Error: Could not find major number in dmesg"
    exit 1
fi

# Create device node if it doesn't exist
if [ ! -e /dev/am335xussetpm ]; then
    mknod /dev/am335xussetpm c "$major" 0
    chmod 666 /dev/am335xussetpm
    echo "/dev/am335xussetpm created with major $major"
else
    echo "/dev/am335xussetpm already exists"
fi


and c program to test it out

/*
setpm.c
Compile: gcc setpm.c -o setpm
*/

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define DEVICE "/dev/am335xussetpm"
#define MEM_START 0x44E10800
#define MEM_END   0x44E109B4

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "setpm: Usage: %s <hex address> <hex value>\n", argv[0]);
        return 1;
    }

    unsigned long addr = strtoul(argv[1], NULL, 16);
    unsigned char value = strtoul(argv[2], NULL, 16) & 0xFF;

    if (addr < MEM_START || addr > MEM_END) {
        fprintf(stderr, "setpm: Error: Address 0x%lx is out of allowed range 0x%lx-0x%lx\n",
                addr, MEM_START, MEM_END);
        return 1;
    }

    unsigned long offset = addr - MEM_START;

    int fd = open(DEVICE, O_RDWR);
    if (fd < 0) { perror("open"); return 1; }

    
    if (pwrite(fd, &value, 1, offset) != 1) {
        perror("setpm: pwrite");
        close(fd);
        return 1;
    }

    printf("setpm: Wrote 0x%02X to address 0x%lx (offset 0x%lx)\n", value, addr, offset);
    close(fd);
    return 0;
}

This is the output showing P8.11 being changed from user space

debian@BeagleBone:~/src/setpm$ pinmux|grep P8.11
gpio0:13 | GPMC_AD13 P8.11         | 0x44e10834 | 0x0834 | 0x37  |    7  | FRXPULLUP
debian@BeagleBone:~/src/setpm$ sudo ./setpm 0x44e10834 0x27
setpm: Wrote 0x27 to address 0x44e10834 (offset 0x34)
debian@BeagleBone:~/src/setpm$ pinmux|grep P8.11
gpio0:13 | GPMC_AD13 P8.11         | 0x44e10834 | 0x0834 | 0x27  |    7  | FRXPULLDN
debian@BeagleBone:~/src/setpm$ sudo ./setpm 0x44e10834 0x37
setpm: Wrote 0x37 to address 0x44e10834 (offset 0x34)
debian@BeagleBone:~/src/setpm$ pinmux|grep P8.11
gpio0:13 | GPMC_AD13 P8.11         | 0x44e10834 | 0x0834 | 0x37  |    7  | FRXPULLUP

Hope this is useful to someone.

2 Likes

Updated the code to more user friendly name
Added dynamic character device registration so that /dev/ shows up on boot

/*
gpio-am335xpm set pinmux mode from user space

Registers a character device dynamically.
Uses class_create and device_create to automatically create /dev/am335xpm.
Maps the restricted physical memory range 0x44E10800–0x44E109B4.
Implements open, release, read, write, mmap.
Includes an exit function to clean up memory mapping and device objects.
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/mm.h>
#include <linux/io.h>
#include <linux/device.h>

#define AM335XPMVER "Ver. 1.01"
#define DEVICE_NAME "am335xpm"
#define MEM_START   0x44E10800 /* Start of memory map */
#define MEM_END     0x44E109B4 /* End of memory map */
#define MEM_SIZE    (MEM_END - MEM_START + 1)

static int major;
static void __iomem *mem_base;
static struct class *am335xpm_class;
static struct device *am335xpm_device;

/* Open/Release */
static int am335xpm_open(struct inode *inode, struct file *file) { return 0; }
static int am335xpm_release(struct inode *inode, struct file *file) { return 0; }

/* Read */
static ssize_t am335xpm_read(struct file *file, char __user *buf,
                                  size_t count, loff_t *ppos)
{
    if (*ppos >= MEM_SIZE)
        return 0;

    if (count + *ppos > MEM_SIZE)
        count = MEM_SIZE - *ppos;

    if (copy_to_user(buf, mem_base + *ppos, count))
        return -EFAULT;

    *ppos += count;
    return count;
}

/* Write */
static ssize_t am335xpm_write(struct file *file, const char __user *buf,
                                   size_t count, loff_t *ppos)
{
    if (*ppos >= MEM_SIZE)
        return -ENOSPC;

    if (count + *ppos > MEM_SIZE)
        count = MEM_SIZE - *ppos;

    if (copy_from_user(mem_base + *ppos, buf, count))
        return -EFAULT;

    *ppos += count;
    return count;
}

/* mmap */
static int am335xpm_mmap(struct file *file, struct vm_area_struct *vma)
{
    unsigned long page_start = MEM_START & PAGE_MASK;
    unsigned long size = PAGE_ALIGN(MEM_SIZE);

    if (vma->vm_end - vma->vm_start > size)
        return -EINVAL;

    if (remap_pfn_range(vma,
                        vma->vm_start,
                        page_start >> PAGE_SHIFT,
                        size,
                        vma->vm_page_prot))
    {
        return -EAGAIN;
    }

    return 0;
}

static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = am335xpm_open,
    .release = am335xpm_release,
    .read    = am335xpm_read,
    .write   = am335xpm_write,
    .mmap    = am335xpm_mmap,
};

/* Initialization */
static int __init am335xpm_init(void)
{
    pr_info("am335xpm: Initializing driver %s\n", AM335XPMVER);

    /* Allocates a major number */
    major = register_chrdev(0, DEVICE_NAME, &fops);
    if (major < 0) {
        pr_err("am335xpm: failed to register char device\n");
        return major;
    }

    /* Create device class */
    am335xpm_class = class_create(DEVICE_NAME);
    if (IS_ERR(am335xpm_class)) {
        unregister_chrdev(major, DEVICE_NAME);
        pr_err("am335xpm: class_create failed\n");
        return PTR_ERR(am335xpm_class);
    }

    /* Create device node /dev/am335xpm */
    am335xpm_device = device_create(am335xpm_class, NULL,
                                         MKDEV(major, 0), NULL, DEVICE_NAME);
    if (IS_ERR(am335xpm_device)) {
        class_destroy(am335xpm_class);
        unregister_chrdev(major, DEVICE_NAME);
        pr_err("am335xpm: device_create failed\n");
        return PTR_ERR(am335xpm_device);
    }

    /* Map physical memory */
    mem_base = ioremap(MEM_START, MEM_SIZE);
    if (!mem_base) {
        device_destroy(am335xpm_class, MKDEV(major, 0));
        class_destroy(am335xpm_class);
        unregister_chrdev(major, DEVICE_NAME);
        pr_err("am335xpm: ioremap failed\n");
        return -ENOMEM;
    }

    pr_info("am335xpm: registered char device major=%d, memory 0x%lx-0x%lx\n",
            major, (unsigned long)MEM_START, (unsigned long)MEM_END);

    return 0;
}

/* Exit cleanup */
static void __exit am335xpm_exit(void)
{
    iounmap(mem_base);
    device_destroy(am335xpm_class, MKDEV(major, 0));
    class_destroy(am335xpm_class);
    unregister_chrdev(major, DEVICE_NAME);
    pr_info("am335xpm: unloaded\n");
}

module_init(am335xpm_init);
module_exit(am335xpm_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("JTBrooks");
MODULE_DESCRIPTION("AM335x memory-mapped driver with automatic /dev node");

updated setpm to read back the written value

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define DEVICE "/dev/am335xpm"
#define MEM_START 0x44E10800
#define MEM_END   0x44E109B4

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "setpm: Usage: %s <hex address> <hex value>\n", argv[0]);
        return 1;
    }

    char *endptr;
    errno = 0;
    unsigned long addr = strtoul(argv[1], &endptr, 16);
    if (errno != 0 || *endptr != '\0') {
        fprintf(stderr, "setpm: Invalid address: %s\n", argv[1]);
        return 1;
    }

    unsigned long value = strtoul(argv[2], &endptr, 16);
    if (errno != 0 || *endptr != '\0') {
        fprintf(stderr, "setpm: Invalid value: %s\n", argv[2]);
        return 1;
    }

    unsigned char byte = value & 0xFF;

    if (addr < MEM_START || addr > MEM_END) {
        fprintf(stderr, "setpm: Error: Address 0x%lx is out of allowed range 0x%lx-0x%lx\n",
                addr, MEM_START, MEM_END);
        return 1;
    }

    unsigned long offset = addr - MEM_START;

    int fd = open(DEVICE, O_RDWR | O_SYNC);
    if (fd < 0) {
        perror("setpm: open");
        return 1;
    }

    // Write the byte
    if (pwrite(fd, &byte, 1, offset) != 1) {
        perror("setpm: pwrite");
        close(fd);
        return 1;
    }

    // Read the byte back for confirmation
    unsigned char read_back;
    if (pread(fd, &read_back, 1, offset) != 1) {
        perror("setpm: pread");
        close(fd);
        return 1;
    }

    if (read_back != byte) {
        fprintf(stderr, "setpm: Warning! Read back value 0x%02X does not match written value 0x%02X\n",
                read_back, byte);
    } else {
        printf("setpm: Wrote:0x%02X Address:0x%lx Offset:0x%lx Actual:0x%02X\n",
               byte, addr, offset, read_back);
    }

    close(fd);
    return 0;
}

and the output

sudo ./setpm 0x44e10834 0x27
setpm: Wrote:0x27 Address:0x44e10834 Offset:0x34 Actual:0x27

pinmux |grep P8.11
gpio0:13 | GPMC_AD13 P8.11         | 0x44e10834 | 0x0834 | 0x27  |    7  | FRXPULLDN

sudo ./setpm 0x44e10834 0x37
setpm: Wrote:0x37 Address:0x44e10834 Offset:0x34 Actual:0x37

pinmux |grep P8.11
gpio0:13 | GPMC_AD13 P8.11         | 0x44e10834 | 0x0834 | 0x37  |    7  | FRXPULLUP