以下是如何在QEMU中实现支持寄存器访问的自定义外部设备的详细步骤和代码示例:


1. 设备代码实现(C语言)


c

复制

#include "hw/sysbus.h"
#include "qemu/timer.h"
#include "qapi/error.h"

#define TYPE_CUSTOM_DEVICE "custom-device"
#define CUSTOM_DEVICE(obj) OBJECT_CHECK(CustomDeviceState, (obj), TYPE_CUSTOM_DEVICE)

typedef struct CustomDeviceState {
    SysBusDevice parent_obj;
    
    // 寄存器定义
    uint32_t reg_data;
    uint64_t rtc_time; // 用于示例的实时时间寄存器
} CustomDeviceState;

// 寄存器偏移量定义
#define REG_DATA        0x00  // 数据寄存器
#define REG_RTC         0x04  // 时间寄存器

// MMIO读回调
static uint64_t custom_read(void *opaque, hwaddr offset, unsigned size) {
    CustomDeviceState *s = opaque;
    uint64_t value = 0;

    switch (offset) {
    case REG_DATA:
        value = s->reg_data;
        break;
    case REG_RTC:
        // 返回当前时间(纳秒转换为毫秒)
        value = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) / 1000000;
        break;
    default:
        qemu_log_mask(LOG_GUEST_ERROR, "%s: invalid offset 0x%" HWADDR_PRIx "\n",
                     __func__, offset);
        break;
    }
    return value;
}

// MMIO写回调
static void custom_write(void *opaque, hwaddr offset, uint64_t value, unsigned size) {
    CustomDeviceState *s = opaque;

    switch (offset) {
    case REG_DATA:
        s->reg_data = value;
        break;
    case REG_RTC:
        // 时间寄存器为只读,忽略写入
        break;
    default:
        qemu_log_mask(LOG_GUEST_ERROR, "%s: invalid offset 0x%" HWADDR_PRIx "\n",
                     __func__, offset);
        break;
    }
}

// 设备初始化
static const MemoryRegionOps custom_ops = {
    .read = custom_read,
    .write = custom_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .valid.min_access_size = 4,
    .valid.max_access_size = 4,
};

static void custom_realize(DeviceState *dev, Error ​**errp) {
    CustomDeviceState *s = CUSTOM_DEVICE(dev);

    // 初始化寄存器
    s->reg_data = 0xDEADBEEF;
    s->rtc_time = 0;

    // 创建MMIO内存区域
    memory_region_init_io(&s->iomem, OBJECT(s), &custom_ops, s,
                          "custom-device", 0x1000); // 映射0x1000字节空间
    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem);
}

static void custom_class_init(ObjectClass *klass, void *data) {
    DeviceClass *dc = DEVICE_CLASS(klass);
    dc->realize = custom_realize;
    dc->desc = "Custom Device with Registers";
}

static const TypeInfo custom_info = {
    .name = TYPE_CUSTOM_DEVICE,
    .parent = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(CustomDeviceState),
    .class_init = custom_class_init,
};

static void custom_register_types(void) {
    type_register_static(&custom_info);
}
type_init(custom_register_types)

2. 编译与集成

  1. 将代码保存为 hw/misc/custom_device.c
  2. 修改QEMU的Makefile
    hw/misc/Makefile.objs 中添加:
    
      

    makefile

    复制

    obj-y += custom_device.o
  3. 重新编译QEMU
    
      

    bash

    复制

    ./configure --enable-debug
    make -j$(nproc)

3. 启动QEMU测试

通过命令行加载设备:


bash

复制

qemu-system-x86_64 \
    -device custom-device \
    -kernel your_kernel_image \
    -append "root=/dev/sda1 console=ttyS0" \
    -hda rootfs.img

4. 用户空间测试程序


c

复制

#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>

#define DEVICE_ADDR 0x50000000  // 假设设备映射地址
#define DEVICE_SIZE 0x1000

int main() {
    int fd = open("/dev/mem", O_RDWR | O_SYNC);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    // 映射设备内存
    void *map = mmap(NULL, DEVICE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, DEVICE_ADDR);
    if (map == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    // 读取数据寄存器
    volatile uint32_t *reg_data = (volatile uint32_t *)(map + 0x00);
    printf("Data Register: 0x%x\n", *reg_data);

    // 读取时间寄存器
    volatile uint64_t *reg_rtc = (volatile uint64_t *)(map + 0x04);
    printf("RTC Time: %llu ms\n", *reg_rtc);

    // 写入数据寄存器
    *reg_data = 0x1234ABCD;
    printf("New Data Value: 0x%x\n", *reg_data);

    munmap(map, DEVICE_SIZE);
    close(fd);
    return 0;
}

编译并运行测试程序:


bash

复制

gcc test.c -o test
sudo ./test

关键点说明

  1. 寄存器映射
    使用 sysbus_init_mmio 将设备内存映射到系统总线,客户机通过物理地址访问。

  2. 时间同步
    示例中的 REG_RTC 返回QEMU虚拟CPU的当前时间(纳秒级精度),可通过 qemu_clock_get_ns 获取。

  3. 错误处理
    使用 qemu_log_mask 记录非法访问,增强设备健壮性。

  4. 访问权限
    MemoryRegionOps 中指定 valid.min/max_access_size 可限制访问粒度。


扩展方向

  • 中断支持:通过 qemu_irq 向客户机发送中断信号。
  • DMA支持:实现DMA控制器接口。
  • 设备树绑定:添加设备树节点描述,便于U-Boot等引导加载器识别。

通过此框架,您可以快速实现复杂的自定义外设逻辑,并在QEMU环境中进行验证。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐