跳转至

GX8002 linux 驱动移植说明*

1. 方案场景概要说明*

GX8002 作为一款低功耗,超小体积的AI芯片,可以满足在戴电池产品下的离线唤醒功能。

​ 比如:学习平板上,当平板进入熄屏休眠状态下,可以只保留 GX8002 在继续工作。一但 GX8002 语音唤醒后,通过GPIO 中断唤醒平板主控。如果需要的话,还可以通过 GX8002SPI 接口,获取唤醒的 2 秒缓存音频,平板做 2级唤醒确认,避免误唤醒。

GX8002 支持空片 OTA 升级,接口支持 UART I2C 两种方式,可以方便硬件上的选择。

GX8002 的芯片信息如下:

2. 推荐的硬件框图*

  • SOC 为平板的主控 CPU。两者公用一个模拟麦克风,可以各取音频数据,互不干扰。

  • 通过 GPIO 来中断唤醒 SOC。中断脉冲长度可以自定义。

  • SPI 用来快速传输唤醒的 2 秒音频,满足 SOC 的 2 级唤醒确认要求。
  • I2C 用于 GX8002 的 OTA 升级功能,和厂测功能,也可以自定义些需要双方交互的功能。
  • SOC 唤醒后,在工作的时候,可以通过 GPIO 断掉 GX8002 的供电,减小功耗。当 SOC 进入休眠的时候,提前开启 GPIO 供电,让 GX8002 进入工作状态。GX8002 开机启动在毫秒级别,速度非常快,100 毫秒以下。

3. 驱动移植指南*

3.1 GX8002 I2C*

​ 交互使用 GX8002 I2C0 这一组。细节可以看第一节的芯片框图。

​ 设备7位地址为: 0x2F,有的平台驱动需要左移一位,为:0x5E

​ 比如获取唤醒源:

​ 唤醒后,向 0xA0 寄存器读一字节,读出 0x64代表 “小x小x” 唤醒事件。这个主要是应对有多个唤醒词的情况下,用来分辨是哪个唤醒词触发了。

​ 我们会提供 linux4.4 上验证好的驱动代码,里面已经包含了全部 I2C 交互的细节,客户只需要移植过去即可。驱动源码包含:

  • 获取唤醒源

  • OTA 升级

  • 获取固件版本号

  • 厂测交互

3.2 GX8002 SPI*

​ SPI 用于传输 GX8002 缓存的 2 秒音频数据,格式为:16K 16bit PCM 音频。

GX8002 的SPI速率最大为4M,推荐使用3M。

GX8002 的SPI传输格式msb,使用spi模式0。

GX8002 SPI 可以做主和从。在这个方案里,推荐 GX8002 做从设备。

GX8002 唤醒后,先对 GPIO0 发送一个脉冲,然后通过 SPI 主动发送 16K 的 ADPCM 音频。

​ Linux 驱动识别到唤醒中断后,通过 SPI 接收 ADPCM 格式的音频数据。并将 ADPCM 解码为 PCM 16K 16bit的 64K 原始音频数据,供平板的应用层通过 linux 标准的 read 接口获取。

3.3 驱动代码移植说明*

​ 解压 gxcodec_8002.zip 文件,设备树文件为:my.dts,readme.txt 包含了编译说明。demo 目录内的代码为 linux 应用层上,如何通过设备文件和 GX8002 进行交互的完整示例。

​ 目前在 linux4.4 上进行测试通过。 gxcodec_8002.zip (http://yun.nationalchip.com:10000/l/9FrkOc)

  • readme.txt】 编译说明文件
  • demo】 应用层使用的参考例程,打开:/dev/gxcodec 进行对应的read和ioctl操作即可
  • gxscpu_boot.hGX8002 OTA 使用的 bootloader 二进制文件
  • gxcodec_main.c】驱动主代码
  • gxcodec_i2c.c】驱动 I2C 代码,包含交互细节
  • gxcodec_spi.c】驱动 SPI 代码,包含交互细节
  • gxcodec_upgrade.cGX8002 OTA I2C 升级协议代码
  • adpcm.c】adpcm音频解码

​ 客户只要自己适当修改下 linux 内核的设备树文件,和该驱动适配,即可达到移植目的。

​ 比如,客户修改要自己的 I2C 接口,GPIO 接口和 SPI 接口。

​ 驱动主函数入口在:gxcodec_main.c,它注册一个平台设备 :

/* Declare a list of devices supported by the driver */
static const struct of_device_id gxcodec_of_match[] = {
    { .compatible = "nationalchip,gxcodec", 0},
    {}
};

MODULE_DEVICE_TABLE(of, gxcodec_of_match);

/* Define platform driver structure */
static struct platform_driver gxcodec_platform_driver = {
    .probe = gxcodec_platform_probe,
    .remove = gxcodec_platform_remove,
    .driver = {
        .name = "gxcodec",
        .of_match_table = gxcodec_of_match,
        .owner = THIS_MODULE,
    }
};

/* Register our platform driver */
module_platform_driver(gxcodec_platform_driver);

​ 设备树参考:

wakeup-gpio 是唤醒的 GPIO,移植时根据主控实际硬件情况选取

power-gpio 是主控给 GX8002 供电的 GPIO,移植时根据主控实际硬件情况选取

gxcodec {
        compatible = "nationalchip,gxcodec";
        wakeup-gpio = <0x37 0x17 0x00>;
        power-gpio = <0x38 0x01 0x00>;
    };

​ 在平台设备的 probe 里,陆续注册一个 GPIO 中断唤醒源,一个供电 GPIO,一个 I2C 驱动,一个 SPI 驱动和一个杂项设备,并增加了一个缓存录音的内核任务。

/* Add probe() function */
static int __init gxcodec_platform_probe(struct platform_device *pdev)
{
    int ret_val, irq;

    pr_info("gxcodec_platform_probe() function is called.\n");
    pr_info("gxcodec driver version - %s\n", DRIVER_VERSION);

    adpcm_buf = devm_kmalloc(&pdev->dev, ENCODEC_DATA_PACKET_SIZE+PCM_VERIFICATION_FLAG_SIZE, GFP_KERNEL | __GFP_REPEAT);
    if (!adpcm_buf) {
        pr_err("Can't allocate param buffer (size = %d)!\n", ENCODEC_DATA_PACKET_SIZE+PCM_VERIFICATION_FLAG_SIZE);
        return -1;
    }
    pcm_buf = devm_kmalloc(&pdev->dev, PCM_BUFFER_SIZE, GFP_KERNEL | __GFP_REPEAT);
    if (!pcm_buf) {
        pr_err("Can't allocate param buffer (size = %d)!\n", PCM_BUFFER_SIZE);
        return -1;
    }

    mutex_init(&p_lock); // 对读取缓存音频进行包含的锁

    ret_val = misc_register(&gx_miscdevice); // 杂项设备驱动注册

    if (ret_val != 0) {
        pr_err("could not register the misc device mydev\n");
        return ret_val;
    }

    pr_info("mydev: got minor %i\n",gx_miscdevice.minor);

    gxcodec_i2c_init(); // I2C driver

    gxcodec_spi_init(); // SPI driver

    // reset chip
    power_gpio_desc = devm_gpiod_get(&pdev->dev, "power", GPIOD_OUT_LOW);
    if (IS_ERR(power_gpio_desc)) {
        pr_err("failed to gpiod get.\n");
        return -1;
    }
    reboot_chip();

 //  初始化录音工作任务
    INIT_WORK(&sv_work, gx_sv_work);

//  唤醒中断源注册
    wakeup_gpio_desc = devm_gpiod_get(&pdev->dev, "wakeup", GPIOD_IN);
    if (IS_ERR(wakeup_gpio_desc)) {
        pr_err("failed to gpiod get.\n");
        return -1;
    }

    irq = gpiod_to_irq(wakeup_gpio_desc);
    if (irq < 0) {
        pr_err("%s get irq error.\n", pdev->name);
    }

    pr_info("%d %s get irq.\n", irq, pdev->name);

    ret_val = devm_request_irq(&pdev->dev, irq, active_irq,IRQF_TRIGGER_FALLING, "active", pdev);
    if (ret_val) {
        pr_err("failed to devm_request_irq irq error:%d\n", ret_val);
    }

    return 0;
}

I2C的设备树参考

gxcodec-i2c@2f {
                compatible = "nationalchip,gxcodec-i2c";
                reg = <0x2f>;
            };
        };

SPI的设备树参考

gxcodec-spi@0 {
                compatible = "nationalchip,gxcodec-spi";
                reg = <0x00>;
                spi-max-frequency = <3000000>;
                status = "okay";
            };

​ gxcodec_main.c的_misc_dev_ioctl是个核心函数,包含了I2C的交互,通过CMD来区分不同的命令,进行交互,包含:获取唤醒消息,获取固件版本号,进行固件升级。

static long misc_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    int res;
    pr_info("misc_dev_ioctl() is called. cmd = %x, arg = %ld\n", cmd, arg);

    switch(cmd) {
    case GET_WAKEUP_ID:
    {
        int event_id = i2c_get_event_id();
        res = copy_to_user((void *)arg, (void *)&event_id, sizeof(event_id));
        break;
    }

    case GET_FW_VERSION:
    {
        int firmware_version = i2c_get_firmware_version();
        res = copy_to_user((void *)arg, (void *)&firmware_version, sizeof(firmware_version));
        break;
    }

    case GET_MIC_STATUS:
    {
        char mic_status = i2c_get_mic_status();
        res = copy_to_user((void *)arg, (void *)&mic_status, sizeof(mic_status));
        break;
    }

    case SET_CHIP_POWER:
    {
        i2c_set_power_status(arg);
        res = 0;
        break;
    }

    case UPGRADE_FIRMWARE:
    {
        if (copy_from_user(&img_info_t, (void *)arg, sizeof(img_info_t))) {
            pr_err("copy_from_user failed!\n");
            return -1;
        }
        pr_info("D:%p, L:%d\n", img_info_t.data, img_info_t.size);
        unsigned char *firmware = kmalloc(img_info_t.size, GFP_KERNEL | __GFP_REPEAT);
        if (!firmware) {
            pr_err("Can't allocate param buffer (size = %d)!\n", img_info_t.size);
            return -1;
        }
        if (copy_from_user(firmware, img_info_t.data, img_info_t.size)) {
            pr_err("copy_from_user failed!\n");
            return -1;
        }

        res = gxcodec_upgrade_firmware(reboot_chip, firmware, img_info_t.size);
        kfree(firmware);
        break;
    }

    default:
        pr_info("invalid cmd\n");
        break;
    }

    return res;
}

misc_dev_read 用来获取驱动缓存的2秒pcm音频。

misc_dev_read 是超时读函数,内部如果没有读取到数据,超时秒后会退出,读取长度为0。

static ssize_t misc_dev_read(struct file *file, char __user *buf, size_t count_want, loff_t *f_pos)
{
    int ret = 0;
    int count = 100;

    pr_info("misc_dev_read() is called\n");

    if (count_want != PCM_BUFFER_SIZE)
    {
        pr_info("count_want must be 1024*64 bytes\n");
        return 0;
    }

    while(count > 0)
    {
        count--;
        mutex_lock(&p_lock);

        if (buffering == 0)
        {
            mutex_unlock(&p_lock);
            msleep(10);
        }
        else if (buffering == 1)
        {
            goto END;
        }
    }

    pr_info("read time out\n");
    return 0;

END:
    if (copy_to_user((void *)buf, (void *)pcm_buf, PCM_BUFFER_SIZE))
    {
        ret = 0;
    }
    else
    {
        ret = PCM_BUFFER_SIZE;
        buffering = 0;
    }
    mutex_unlock(&p_lock);

    return ret;
}

3.4 ioctl cmd说明*

目前支持5个cmd,参考demo.c 。

  • ​ #define GET_WAKEUP_ID 0x0800 //获取唤醒事件

唤醒中断触发后,获取到唤醒事件id, 小布小布唤醒事件id=100

  • ​ #define GET_FW_VERSION 0x0801 //获取固件版本号

版本号由4个byte组成,如:v0.0.0.1

  • ​ #define GET_MIC_STATUS 0x0802 //获取mic状态,供测试mic用

此功能是用了芯片vad功能测试mic链路是否通,在有外音的情况下,读取到的值1为正常,0为异常

  • ​ #define SET_CHIP_POWER 0x0803 //设置芯片的电源

  • 若使用通用gpio控制着电源,则定义宏#define CHIP_POWER_BY_GPIO

    需要根据硬件实际情况设备树配置: power-gpio

    代码设置 CHIP_POWER_ENABLE_SET_VALUE 01

  • 如使用PMU或者其他方式,则注释宏#define CHIP_POWER_BY_GPIO

    驱动里补充接口

    void enable_chip(void)
    void disable_chip(void)
    void reboo_chip(void)
    
  • ​ #define UPGRADE_FIRMWARE 0x0888 //升级固件

需要根据硬件实际情况配置宏#define GXCODEC_I2C_DATA_ADDRESS 0x36 //boot i2c addr, decision: GPIO2 low->0x35, GPIO2 high->0x36

芯片上电瞬间会检测 gpio 电平,低电平时地址为0x35,高电平时为0x36