/*
* CHU - Module for driving CH375 in disk mode from Linux 2.0/2.2
* Copyright (C) 2026 Javier S. Pedro
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
/** how many different units we can handle, i.e. /dev/chuda, chudb, etc.*/
#define CHUDISK_MAX_UNITS 1
/** how many bits of the minor number are dedicated to partition number */
#define CHUDISK_MINOR_SHIFT 6
/** how many different partitions we can handle (/dev/chuda1, chuda2, etc.)
* which comes from the number of bits available in the minor number. */
#define CHUDISK_MAX_PARTITIONS (1 << CHUDISK_MINOR_SHIFT)
/** total number of minors we can handle. */
#define CHUDISK_MAX_MINORS (CHUDISK_MAX_UNITS << CHUDISK_MINOR_SHIFT)
/* module parameters */
/** configurable major number */
/* by default, reuse one in the experimental space */
static short major = 63;
/** IO base */
static short io = 0x260;
/** skip device proving */
static char force = 0;
/** configurable block size */
static int softblk = 1024;
/* for blk.h: (needs to be defined before blk.h/lnxabs.h is included) */
#define MAJOR_NR major
#define DEVICE_NAME "chud"
#define DEVICE_REQUEST do_chudisk_request
#define DEVICE_NR(device) (MINOR(device)>>CHUDISK_MINOR_SHIFT)
#define DEVICE_ON(device)
#define DEVICE_OFF(device)
#include "lnxabs.h"
#include "ch375lnx.h"
#include
#include
#if ENABLE_DLOG
#define dprintf(args...) printk(KERN_DEBUG args)
#else
#define dprintf(args...) /**/
#endif
MODULE_PARM(major, "h");
MODULE_PARM_DESC(major, "major device number");
MODULE_PARM(io, "h");
MODULE_PARM_DESC(io, "io base");
MODULE_PARM(force, "b");
MODULE_PARM_DESC(force, "force loading, skip probing");
MODULE_PARM(softblk, "i");
MODULE_PARM_DESC(softblk, "\"soft\" block size in bytes");
MODULE_DESCRIPTION("CHUDISK CH375 USB disk driver");
MODULE_LICENSE("GPL");
/** hd_structs, used by gendisk to keep track of partition sizes, start points, etc. */
static struct hd_struct chudisk_structs[CHUDISK_MAX_MINORS];
/** used by blk layer to keep track of size of every (minor) blk device. */
static int chudisk_sizes[CHUDISK_MAX_MINORS] = {0};
/** the block size (possibly different for every minor, but we really don't do that) */
static int chudisk_blksizes[CHUDISK_MAX_MINORS] = {0};
/** hardware block size */
static int chudisk_hardsectsizes[CHUDISK_MAX_MINORS] = {0};
/** count open for each unit (not partition) */
static int chudisk_open_count[CHUDISK_MAX_UNITS] = {0};
/** driver data for each unit */
static struct ch375 chudisk_dev[CHUDISK_MAX_UNITS];
/** main gendisk layer data structure, which handles partioning */
static struct gendisk chudisk_gendisk = {
.major_name = DEVICE_NAME,
.minor_shift = CHUDISK_MINOR_SHIFT,
.max_p = CHUDISK_MAX_PARTITIONS,
.max_nr = CHUDISK_MAX_UNITS,
.part = chudisk_structs,
.sizes = chudisk_sizes
};
/** Waits for the CH375 to generate an interrupt, returns result value.
* @param timeout max jiffies to wait for an interrupt */
static uint8_t wait_for_ch375_result(struct ch375 *dev, unsigned int timeout)
{
unsigned long start = jiffies;
unsigned long expire = start + timeout;
while (!ch375_poll_interrupt(dev) && time_before(jiffies, expire)) {
}
if (ch375_poll_interrupt(dev)) {
uint8_t status = ch375_get_int_status(dev);
return status;
} else {
printk(KERN_WARNING "chudisk: interrupt timeout! waited %lu jiffies\n", jiffies - start);
return 0;
}
}
/** Gets (error) sense data from the USB device (if any)
* and prints it to the log. */
static void disk_sense_error(struct ch375 *dev)
{
char buffer[CH375_BLOCK_SIZE];
unsigned len;
ch375_send_command(dev, CH375_DISK_R_SENSE);
if (wait_for_ch375_result(dev, 1000) != CH375_INT_USB_SUCCESS) {
printk(KERN_NOTICE "chudisk: could not sense error\n");
return;
}
ch375_send_command(dev, CH375_RD_USB_DATA);
len = ch375_read_data_block(dev, &buffer[0]);
printk(KERN_NOTICE "chudisk: sense read data block len = %d\n", len);
printk(KERN_NOTICE "chudisk: sense packet %hx %hx %hx %hx %hx %hx %hx %hx\n",
buffer[0], buffer[1], buffer[2], buffer[3],
buffer[4], buffer[5], buffer[6], buffer[7]);
printk(KERN_NOTICE "chudisk: %hx %hx %hx %hx %hx %hx %hx %hx\n",
buffer[8], buffer[9], buffer[10], buffer[11],
buffer[12], buffer[13], buffer[14], buffer[15]);
}
/** Read N sectors from the disk
* @return number of sectors read
*/
static unsigned disk_read_sectors(struct ch375 *dev, uint8_t *buffer, uint32_t lba, uint8_t num)
{
uint8_t result;
unsigned total = 0; // Total bytes written
dprintf("disk read buf=%p sector=%u num=%hu\n", buffer, lba, num);
// Initiate read operation
ch375_disk_read(dev, lba, num);
// Now keep going as long as ch375 says to keep going
while ((result = wait_for_ch375_result(dev, 1000)) == CH375_INT_USB_DISK_READ) {
unsigned len;
// Sanity check
if (total / dev->sec_size >= num) {
dprintf("read overflow\n");
break;
}
ch375_send_command(dev, CH375_RD_USB_DATA);
len = ch375_read_data_block(dev, &buffer[total]);
//dprintf(" disk read data block len = %d\n", len);
if (len == 0) {
// Done?
break;
}
total += len;
// Continue
ch375_send_command(dev, CH375_DISK_RD_GO);
}
dprintf(" disk read total bytes = %u, sectors = %u, final status = 0x%hx\n",
total, total / dev->sec_size, result);
if (result == CH375_INT_USB_DISK_ERR) {
disk_sense_error(dev);
}
return total / dev->sec_size;
}
/** Write N sectors to the disk
* @return number of sectors written
*/
static unsigned disk_write_sectors(struct ch375 *dev, uint8_t *buffer, uint32_t lba, uint8_t num)
{
uint8_t result;
unsigned total = 0; // Total bytes written
dprintf("disk write buf=%p sector=%u num=%hu\n", buffer, lba, num);
// Initiate operation
ch375_disk_write(dev, lba, num);
// Now keep going as long as ch375 says to keep going
while ((result = wait_for_ch375_result(dev, 1000)) == CH375_INT_USB_DISK_WRITE) {
const unsigned len = CH375_BLOCK_SIZE;
// Sanity check
if (total / dev->sec_size >= num) {
dprintf("write overflow\n");
break;
}
ch375_send_command(dev, CH375_WR_USB_DATA7);
ch375_write_data_block(dev, &buffer[total], len);
total += len;
// Continue
ch375_send_command(dev, CH375_DISK_WR_GO);
}
dprintf(" disk write total bytes = %u, sectors = %u, final status = 0x%hx\n",
total, total / dev->sec_size, result);
if (result == CH375_INT_USB_DISK_ERR) {
disk_sense_error(dev);
}
return total / dev->sec_size;
}
/** Enables CH375 USB host mode, initializes "disk" mode on it
* and connects to a disk.
* @return true if connected
*/
static bool chudisk_connect(struct ch375 *dev)
{
int tries;
dprintf("switch to usb host\n");
ch375_set_usb_mode(dev, CH375_USB_MODE_HOST_AUTO_SOF);
if (wait_for_ch375_result(dev, HZ / 5) != CH375_INT_USB_CONNECT) {
printk(KERN_INFO "chudisk: could not turn on usb host, likely no device\n");
return false;
}
dprintf("try init disk\n");
ch375_disk_init(dev);
if (wait_for_ch375_result(dev, HZ) != CH375_INT_USB_SUCCESS) {
printk(KERN_INFO "chudisk: could not init disk\n");
return false;
}
// looks like we have to keep polling by asking "is the disk ready?"
// even if each time we ask we are already polling
tries = 3;
while (tries-- > 0) {
ch375_test_disk_ready(dev);
if (wait_for_ch375_result(dev, HZ) == CH375_INT_USB_SUCCESS) {
dprintf("disk now init\n");
return true;
}
mdelay(50);
}
printk(KERN_INFO "chudisk: disk failed to init\n");
return false;
}
/** Tests if there is still a disk connected and initialized. */
static bool is_chudisk_ready(struct ch375 *dev)
{
ch375_test_disk_ready(dev);
if (wait_for_ch375_result(dev, HZ/10) == CH375_INT_USB_SUCCESS) {
return true;
} else {
return false;
}
}
/** Actual main I/O function, called whenever there are pending I/O requests */
static void do_chudisk_request(void)
{
while (CURRENT) {
int rc = 0;
struct hd_struct *hd_struct;
INIT_REQUEST;
hd_struct = &chudisk_structs[MINOR(CURRENT->rq_dev)];
dprintf("do_chudisk_request: cmd %u sector %lu(/%lu) nr_sectors %lu/%lu\n",
CURRENT->cmd, CURRENT->sector, hd_struct->nr_sects, CURRENT->current_nr_sectors, CURRENT->nr_sectors);
// If:
// the request is for one of our devices
// and it is in-range of the corresponding partition
// and respects our max_nr_sectors (256)
if (CURRENT_DEV < CHUDISK_MAX_UNITS
&& CURRENT->sector + CURRENT->nr_sectors <= hd_struct->nr_sects
&& CURRENT->current_nr_sectors <= 0xff) {
struct ch375 *dev = &chudisk_dev[CURRENT_DEV];
uint32_t sector = CURRENT->sector + hd_struct->start_sect;
uint8_t cnt = CURRENT->current_nr_sectors;
switch (CURRENT->cmd) {
case READ:
if (disk_read_sectors(dev, CURRENT->buffer, sector, cnt) == cnt) {
rc = true; // success
}
break;
case WRITE:
if (disk_write_sectors(dev, CURRENT->buffer, sector, cnt) == cnt) {
rc = true; // success
}
break;
default:
break;
}
}
end_request(rc);
}
}
/** Called by gendisk to check if removable media may have changed since last time.
* If so, gendisk will invalidate buffers and eventually call revalidate.
* @return true if media may have changed
*/
static int chudisk_check_media_change(kdev_t dev)
{
int unit = DEVICE_NR(dev);
bool ready;
dprintf("chudisk_check_media_change: unit=%d\n", unit);
if (unit >= CHUDISK_MAX_UNITS) {
return -ENODEV;
}
ready = is_chudisk_ready(&chudisk_dev[unit]);
dprintf("chudisk_check_media_change: unit=%d disk_ready=%d\n", unit, ready);
// if CH375 is not ready, it means device was unplugged (or never configured to begin with)
// assume media changed
return !ready;
}
/** Revalidate means drop all buffers (assuming media changed)
* reinitialize all media information (sizes, etc.)
* and re-read partition tables. */
static int chudisk_revalidate(kdev_t devno)
{
int unit = DEVICE_NR(devno);
struct ch375 *dev = &chudisk_dev[unit];
uint32_t blk_size;
uint32_t sector_cnt;
char buffer[CH375_BLOCK_SIZE];
int p;
dprintf("chudisk_revalidate: unit=%d\n", unit);
if (unit >= CHUDISK_MAX_UNITS) {
return -ENODEV;
}
if (!is_chudisk_ready(dev)) {
if (!chudisk_connect(dev)) {
return -ENOMEDIUM;
}
}
ch375_send_command(dev, CH375_DISK_SIZE);
if (wait_for_ch375_result(dev, HZ / 10) != CH375_INT_USB_SUCCESS) {
return -EIO;
}
ch375_send_command(dev, CH375_RD_USB_DATA);
if (ch375_read_data_block(dev, buffer) < 8) {
printk(KERN_WARNING "chudisk: disk size command return less than 8 bytes");
return -EIO;
}
blk_size = ntohl(*(uint32_t*)&buffer[4]);
sector_cnt = ntohl(*(uint32_t*)&buffer[0]);
dprintf("chudisk[%d]: sector size %u num sectors %u\n", unit, blk_size, sector_cnt);
// TODO Support non-default block_size
dev->sec_size = blk_size;
ch375_write_reg(dev, CH375_REG_DISK_PKT_P_SEC, blk_size / CH375_BLOCK_SIZE);
// Fill in the struct for the raw device, so that we can answer I/O requests on it
chudisk_structs[unit << CHUDISK_MINOR_SHIFT].nr_sects = sector_cnt;
// For all minors associated to this unit
for (p = 0; p < CHUDISK_MAX_PARTITIONS; p++) {
int minor = (unit << CHUDISK_MINOR_SHIFT) + p;
kdev_t mdevno = MKDEV(MAJOR_NR, minor);
fsync_dev(mdevno);
invalidate_inodes_dev(mdevno);
invalidate_buffers(mdevno);
chudisk_hardsectsizes[minor] = blk_size;
}
resetup_one_dev(&chudisk_gendisk, unit);
return 0;
}
/** Fill in guessed hd_geometry from the current media size. */
static int chudisk_get_geom(kdev_t devno, struct hd_geometry *g)
{
struct hd_struct *hd_struct = &chudisk_structs[DEVICE_NR(devno)];
struct hd_struct *hd_struct_part = &chudisk_structs[MINOR(devno)];
unsigned int cylinders, heads, sectors;
if (hd_struct->nr_sects == 0 || hd_struct_part->nr_sects == 0) {
return -ENOMEDIUM;
}
guess_geometry(hd_struct->nr_sects, &cylinders, &heads, §ors);
g->cylinders = cylinders > 0xFFFF ? 0xFFFF : cylinders;
g->heads = heads;
g->sectors = sectors;
g->start = hd_struct_part->start_sect;
return 0;
}
/** Ioctl handling. */
static int chudisk_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
int err;
dprintf("chudisk_ioctl: unit=%d cmd=%u arg=%lu\n", DEVICE_NR(inode->i_rdev), cmd, arg);
if (DEVICE_NR(inode->i_rdev) >= CHUDISK_MAX_UNITS) {
return -ENXIO;
}
switch (cmd) {
case HDIO_GETGEO: {
struct hd_geometry g;
if (!arg) return -EINVAL;
if ((err = chudisk_get_geom(inode->i_rdev, &g))) {
return err;
}
return copy_to_user((struct hd_geometry*)arg, &g, sizeof g) ? -EFAULT : 0;
}
case BLKRASET:
if (!is_sys_admin()) return -EACCES;
if (arg > 0xff) return -EINVAL;
read_ahead[MAJOR(inode->i_rdev)] = arg;
return 0;
case BLKRAGET:
if (!arg) return -EINVAL;
return put_user(read_ahead[MAJOR(inode->i_rdev)], (long *)arg);
case BLKGETSIZE:
if (!arg) return -EINVAL;
return put_user(chudisk_structs[MINOR(inode->i_rdev)].nr_sects, (long*)arg);
case BLKFLSBUF:
if (!is_sys_admin()) return -EACCES;
fsync_dev(inode->i_rdev);
invalidate_buffers(inode->i_rdev);
return 0;
case BLKRRPART:
if (!is_sys_admin()) return -EACCES;
return chudisk_revalidate(inode->i_rdev);
RO_IOCTLS(inode->i_rdev, arg);
default:
return -EINVAL;
}
}
static int chudisk_open(struct inode *inode, struct file *file)
{
int unit = DEVICE_NR(inode->i_rdev);
dprintf("chudisk_open: unit=%d\n", unit);
if (unit >= CHUDISK_MAX_UNITS) {
return -ENXIO;
}
MOD_INC_USE_COUNT;
if (chudisk_open_count[unit] == 0) {
// First time open
dprintf("chudisk: first open\n");
// Ask gendisk to check if media may have changed (which may call revalidate)
check_disk_change(inode->i_rdev);
// if we still have no media, refuse open() call
if (!is_chudisk_ready(&chudisk_dev[unit])) {
MOD_DEC_USE_COUNT;
return -ENOMEDIUM;
}
}
chudisk_open_count[unit]++;
return 0;
}
static int chudisk_release(struct inode *inode, struct file *file)
{
int unit = DEVICE_NR(inode->i_rdev);
dprintf("chudisk_release: unit=%d\n", unit);
if (unit >= CHUDISK_MAX_UNITS) {
return -ENXIO;
}
chudisk_open_count[unit]--;
MOD_DEC_USE_COUNT;
return 0;
}
static struct file_operations blk_fops = {
.read = block_read,
.write = block_write,
.ioctl = chudisk_ioctl,
.open = chudisk_open,
.release = chudisk_release,
.check_media_change = chudisk_check_media_change,
.revalidate = chudisk_revalidate
};
static __init int chudisk_init(void)
{
// TODO Here we are currently assuming one unit
// But it would be trivial to add support for more.
const int unit = 0;
struct ch375 *dev = &chudisk_dev[unit];
int i;
dev->io_base = io;
dev->sec_size = 512;
if (check_region(io, 2)) {
printk(KERN_WARNING "chudisk: ports at 0x%x not available\n", io);
return -ENXIO;
}
request_region(io, 2, "chudisk");
if (!force) {
dprintf("chudisk: resetting CH375 at 0x%hx\n", dev->io_base);
ch375_reset(dev);
mdelay(100);
if (ch375_check_exists(dev, 0xA5) != (uint8_t)~0xA5) {
printk(KERN_WARNING "chudisk: CH375 not detected at 0x%hx (failed CHECK_EXISTS), change io= param\n", dev->io_base);
release_region(io, 2);
return -ENODEV;
}
}
if (register_blkdev(MAJOR_NR, "chud", &blk_fops)) {
printk(KERN_WARNING "chudisk: unable to get major number %d\n", MAJOR_NR);
release_region(io, 2);
return -ENXIO;
}
printk(KERN_INFO "chudisk: v%x.%x, found CH375 v.%x at 0x%hx, using major %d\n",
CHUDISK_VERSION_MAJOR, CHUDISK_VERSION_MINOR,
ch375_get_version(dev), dev->io_base, MAJOR_NR);
// Set defaults for unit
for (i = 0; i < CHUDISK_MAX_PARTITIONS; i++) {
const int minor = (unit << CHUDISK_MINOR_SHIFT) + i;
chudisk_sizes[minor] = 0;
chudisk_blksizes[minor] = softblk;
chudisk_hardsectsizes[minor] = dev->sec_size;
}
// Set defaults for everything else
read_ahead[MAJOR_NR] = 8; // default to 8 sector read-ahead
blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
blk_size[MAJOR_NR] = chudisk_sizes;
blksize_size[MAJOR_NR] = chudisk_blksizes;
hardsect_size[MAJOR_NR] = chudisk_hardsectsizes;
// Add ourselves to the genhd linked list
chudisk_gendisk.major = MAJOR_NR;
chudisk_gendisk.nr_real = 1;
chudisk_gendisk.next = gendisk_head;
gendisk_head = &chudisk_gendisk;
chudisk_revalidate(MKDEV(MAJOR_NR, unit << CHUDISK_MINOR_SHIFT));
return 0;
}
static void chudisk_exit(void)
{
struct ch375 *dev = &chudisk_dev[0];
struct gendisk **gdp;
int i;
// Flush buffers for all minors
for (i = 0; i < CHUDISK_MAX_MINORS; i++) {
kdev_t mdevno = MKDEV(MAJOR_NR, i);
fsync_dev(mdevno);
invalidate_inodes_dev(mdevno);
invalidate_buffers(mdevno);
}
// Remove from genhd list
for(gdp = &gendisk_head; *gdp; gdp = &((*gdp)->next)) {
if (*gdp == &chudisk_gendisk) {
*gdp = (*gdp)->next;
break;
}
}
// Remove from blkdev
blk_dev[MAJOR_NR].request_fn = NULL;
blk_size[MAJOR_NR] = NULL;
blksize_size[MAJOR_NR] = NULL;
hardsect_size[MAJOR_NR] = NULL;
unregister_blkdev(MAJOR_NR, "chud");
// Turn off the chip
ch375_set_usb_mode(dev, CH375_USB_MODE_HOST_DISABLED);
mdelay(1);
ch375_enter_sleep(dev);
// And unregister driver
release_region(io, 2);
dprintf("chudisk: bye\n");
}
module_init(chudisk_init);
module_exit(chudisk_exit);