K230-GNNE驱动移植

硬件平台

K230架构图如下:

K230架构

黄框是AI Subsystem,里面包含了KPU、AI2D、FFT以及SRAM,这里我们只考虑KPU和AI2D。

GNNE

GNNE,或者叫KPU,这两个在嘉楠手册的Memory Map中是同一个地址,也就是是同一个外设。

KPU用于实现神经网络算子,K230的KPU目前支持:int8和int16的卷积、池化,LSTM,RELU等算子以及一些矩阵运算。但是Solfmax等算子还是得交给CPU进行计算。具体支持算子详见:K230技术参考手册4.2 Function Description部分。

AI2D

AI2D模块主要进行图像处理,可以对来自CPU或者KPU的数据进行缩放、仿射变化、裁剪、填充等。

驱动架构设计

查阅K230技术参考手册,会发现一个问题,嘉楠并没有提供KPU的寄存器手册,和工程师沟通后,了解到GNNE的驱动分为两部分,一部分包含锁以及轮询等待,另一部分由NNCASE实现,直接在用户态进行IO内存映射。因此,想完成在K230上运行yolo之类的神经网络模型,需要三部分:NNCASE、MMZ驱动、GNNE(KPU+AI2D)驱动。

NNCASE

NNCASE是嘉楠开发的神经网络编译器,支持CPU和KPU,AI2D作为一个子模块包含在里面的。nncase大致分为两部分,一个是编译器compiler和runtime。

由于笔者在驱动移植过程中主要负责bsp部分,nncase并没有深入研究,使用方法详见:K230 nncase开发指南,嘉楠镜像中也有设计好的的demo:AI 应用开发,在menuconfigK230 SDK Project Configuration ->RT-Smart Configuration -> Enable build Rtsmart examples中勾选即可:

嘉楠镜像使能ai demo

烧录后进入系统,cd app/examples进入工程目录执行对应历程的脚本或者elf即可。

MMZ驱动

驱动写好后,直接运行demo会报一个很奇怪的错误:

image-20251226174641753

大致原因似乎是gp指针为0,然后有一个gp-0x800的寻址,导致指向了0xfffffffffffff800这个内核空间,并不是驱动报错。

和嘉楠工程师沟通后,发现缺少一个mmz驱动,具体在这里:mpp驱动,mmz是整个驱动下面的一个子模块。mmz驱动并没开源,嘉楠提供的是一个静态库,简单看了一下mmz的使用demo:

驱动的作用是分配非缓存内存以及带缓存的内存,以及物理/虚拟地址映射。大致猜测了一下nncase为什么会对mmz有依赖:

  • 硬件加速器(GNNE、AI2D)需要物理地址进行 DMA 传输
  • 需要大块连续物理内存
  • 用户空间和硬件之间零拷贝共享数据
  • NNCASE需要在用户态进行io内存映射

GNNE(KPU)驱动

之前提到,具体的算子实现以及内存分配由NNCASE和MMZ实现了,在GNNE驱动中,只需要实现阻塞和锁以及中断就行了。所以,对于GNNE和AI2D驱动的ops,需要有如下功能:openclosepollcontrol,仔细看rtt设备子系统的结构体rt_device_ops

1
2
3
4
5
6
7
8
9
10
struct rt_device_ops
{
/* common device interface */
rt_err_t (*init) (rt_device_t dev);
rt_err_t (*open) (rt_device_t dev, rt_uint16_t oflag);
rt_err_t (*close) (rt_device_t dev);
rt_ssize_t (*read) (rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size);
rt_ssize_t (*write) (rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size);
rt_err_t (*control)(rt_device_t dev, int cmd, void *args);
};

openclosecontrol,但是没有poll,所以把GNNE当作一个设备子系统注册,dfs_file_ops恰好能满足这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct dfs_file_ops
{
int (*open)(struct dfs_file *file);
int (*close)(struct dfs_file *file);
int (*ioctl)(struct dfs_file *file, int cmd, void *arg);
ssize_t (*read)(struct dfs_file *file, void *buf, size_t count, off_t *pos);
ssize_t (*write)(struct dfs_file *file, const void *buf, size_t count, off_t *pos);
int (*flush)(struct dfs_file *file);
off_t (*lseek)(struct dfs_file *file, off_t offset, int wherece);
int (*truncate)(struct dfs_file *file, off_t offset);
int (*getdents)(struct dfs_file *file, struct dirent *dirp, uint32_t count);
int (*poll)(struct dfs_file *file, struct rt_pollreq *req);

int (*mmap)(struct dfs_file *file, struct lwp_avl_struct *mmap);
int (*lock)(struct dfs_file *file, struct file_lock *flock);
int (*flock)(struct dfs_file *file, int, struct file_lock *flock);
};

dfs文件系统的ops提供了ioctl可以实现硬件层面的锁,poll可以实现阻塞等待,因此,GNNE被当作一个文件注册驱动。

实现细节

GNNE和AI2D的驱动基本完全相同,虽然K230手册中提供了AI2D模块的寄存器,但是事实上AI2D的内存映射还是在NNCASE中完成的,驱动只是提供阻塞和锁等等。由于AI2D的实现和GNNE基本完全一致,这里不再讨论。

GNNE驱动实现

首先需要在驱动里实现dfs_files_ops这个结构体:

1
2
3
4
5
6
7
static const struct dfs_file_ops gnne_input_fops =
{
.open = gnne_device_open,
.close = gnne_device_close,
.ioctl = gnne_device_ioctl,
.poll = gnne_device_poll,
};

gnne_device_open函数用于为每个线程创建独立的句柄,获取设备对象并保存队列等待指针,初始化锁状态为未锁定,初始化锁,最后将句柄绑定到文件描述符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int gnne_device_open(struct dfs_file *file)
{
struct gnne_dev_handle *handle;
rt_device_t device;

handle = rt_malloc(sizeof(struct gnne_dev_handle));
if (handle == RT_NULL)
{
gnne_err("malloc failed\n");
return -1;
}
device = (rt_device_t)file->vnode->data;
handle->wait = &device->wait_queue;
handle->is_lock = RT_FALSE;
file->data = (void *)handle;
return RT_EOK;
}

gnne_dev_handle结构体有两个成员,is_lock是锁,而wait是阻塞等待队列。

1
2
3
4
5
struct gnne_dev_handle
{
rt_wqueue_t *wait;
rt_bool_t is_lock;
};

gnne_device_close函数用于设备的关闭,在用户空间调用 close(fd) 时被触发,负责清理资源并释放硬件锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int gnne_device_close(struct dfs_file *file)
{
struct gnne_dev_handle *handle;

handle = (struct gnne_dev_handle *)file->data;
if (handle == RT_NULL)
{
gnne_err("try to close a invalid handle");
return -RT_EINVAL;
}
if (handle->is_lock)
{
kd_hardlock_unlock(g_kpu_lock);
}
rt_free(handle);
file->data = RT_NULL;
return RT_EOK;
}

锁的实现在gnne_device_ioctl中,用户空间通过对ioctl() 的调用,实现 GNNE 硬件的访问控制。GNNE驱动的锁控制有三种模式:GNNE_CMD_LOCK用于实现自旋锁,GNNE_CMD_UNLOCK实现对硬件的解锁,而GNNE_CMD_TRYLOCK只尝试尝试一次上锁,并不进行自旋,所有的锁实现后,把值赋给每个线程字节的锁gnne_dev_handle->is_lock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static int gnne_device_ioctl(struct dfs_file *file, int cmd, void *args)
{
struct gnne_dev_handle *handle;
int ret = -1;
handle = (struct gnne_dev_handle *)file->data;
if ((g_kpu_lock == HARDLOCK_MAX)){
return ret;
}
if (cmd == GNNE_CMD_LOCK){
if (handle->is_lock == RT_TRUE){
return 0;
}
while(kd_hardlock_lock(g_kpu_lock));
handle->is_lock = RT_TRUE;
ret = 0;
}else if (cmd == GNNE_CMD_UNLOCK){
if (handle->is_lock == RT_FALSE){
return 0;
}
kd_hardlock_unlock(g_kpu_lock);
handle->is_lock = RT_FALSE;
ret = 0;
}else if (cmd == GNNE_CMD_TRYLOCK){
if (handle->is_lock == RT_TRUE){
return 0;
}
if (!kd_hardlock_lock(g_kpu_lock)){
handle->is_lock = RT_TRUE;
ret = 0;
}
}
return ret;
}

如果学过操作系统关于竞争与冒险这一部分,应该知道,上锁的语句必须是原子性的,kd_hardlock_lock(g_kpu_lock)的实现如下(hardlock/drv_hardlock.c):

1
2
3
4
5
6
7
8
9
10
11
int kd_hardlock_lock(hardlock_type num)
{
if(num < 0 || num >= HARDLOCK_MAX)
return -1;
if(!readl(hardlock.hw_base + num * 0x4)){
LOG_D("hardlock-%d locked\n", num);
return 0;
}
LOG_D("hardlock-%d is busy\n", num);
return -1;
}

核心是通过readl(hardlock.hw_base + num * 0x4)这个原子性的操作实现上锁,g_kpu_lockhardlock/drv_hardlock.h被定义为2,hardlock.hw_base指向Mailbox基地址的0x00A0,也就是这个寄存器:image-20251226213106191

通过num*0x04进行32位寄存器的寻址,如果读到是0,则为上锁。相比于kd_hardlock_lock(hardlock_type num)kd_hardlock_unlock(hardlock_type num)多了一句写0解锁的操作。

1
2
3
4
5
6
7
8
9
10
11
void kd_hardlock_unlock(hardlock_type num)
{
if(num < 0 || num >= HARDLOCK_MAX)
return;

if(readl(hardlock.hw_base + num * 0x4))
{
writel(0x0, hardlock.hw_base + num * 0x4);
}
LOG_D("hardlock-%d unlock\n", num);
}

gnne_device_poll实现了GNNE的poll接口,调用gnne_input_fops.poll后,调用rt_event_recv进入阻塞,释放CPU,等待GNNE硬件完成计算,中断唤醒这个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int gnne_device_poll(struct dfs_file *file, struct rt_pollreq *req)
{
struct gnne_dev_handle *handle;
unsigned int flags;
handle = (struct gnne_dev_handle *)file->data;
if (!handle)
{
gnne_err("gnne_dev_handle NULL!");
return -EINVAL;
}
rt_event_recv(&g_gnne_event, 0x01, RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, NULL);
rt_poll_add(handle->wait, req);
return POLLIN;
}

唤醒的事件来自irq_callback,设备就绪的中断发出后,中断回调函数清空标志位,唤醒等待队列并发送事件,唤醒阻塞中的gnne_device_poll。其中,__iowmb()是内存屏障,确保在清除中断标志之前,所有之前的内存操作都已完成,防止编译器或 CPU 重排序导致中断标志过早清除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void irq_callback(int irq, void *data)
{
rt_wqueue_t *wait = (rt_wqueue_t *)data;
volatile void *write_addr = (void *)((char *)gnne_base_addr + 0x128);
if (gnne_base_addr == RT_NULL)
{
gnne_err("gnne interrupts while the hardware is not yet initialized\n");
}
/*clear kpu intr*/
__iowmb();
*(rt_uint64_t *)write_addr = 0x400000004;
rt_wqueue_wakeup(wait, (void *)POLLIN);
rt_event_send(&g_gnne_event, 0x1);
}

整体调用流程大致如下:

sequenceDiagram
    participant User as 用户进程
    participant Poll as gnne_device_poll()
    participant Event as rt_event_recv()
    participant HW as GNNE 硬件
    participant IRQ as irq_callback()
    
    User->>Poll: 调用 poll()
    Poll->>Event: rt_event_recv() 阻塞等待
    Note over Event: 等待事件 g_gnne_event
    
    HW->>HW: 完成计算
    HW->>IRQ: 触发中断
    
    IRQ->>IRQ: 清除中断标志<br/>*(0x128) = 0x400000004
    IRQ->>Event: rt_event_send(g_gnne_event)
    Note over Event: 事件到达,唤醒
    
    Event-->>Poll: 返回
    Poll->>Poll: rt_poll_add() 注册等待队列
    Poll-->>User: return POLLIN
    Note over User: poll() 返回,数据可读

需要注意的是,这个实现与标准的 poll 机制有所不同:

  • 标准 poll:先注册等待队列,如果没有数据则返回 0,有数据时通过等待队列唤醒
  • 这个实现:先通过 rt_event_recv 同步等待,等待完成后才注册等待队列

这种设计可能是因为: 1. 实际的同步机制完全依赖事件机制(rt_event_recv / rt_event_send) 2. rt_poll_add 只是为了满足 poll 框架的接口要求 3. 具体的时序要求与 NNCASE 的调用方式有关

这使得 poll 调用总是阻塞的,更像是一个”同步等待硬件完成”的接口,而不是传统的非阻塞轮询。


K230-GNNE驱动移植
https://chuann-sudo.github.io/2025/12/25/K230-GNNE驱动移植/
作者
ChuanN
发布于
2025年12月25日
许可协议