GX8002 linux 驱动移植说明*
1. 方案场景概要说明*
GX8002 作为一款低功耗,超小体积的AI芯片,可以满足在戴电池产品下的离线唤醒功能。
比如:学习平板上,当平板进入熄屏休眠状态下,可以只保留 GX8002 在继续工作。一但 GX8002 语音唤醒后,通过GPIO 中断唤醒平板主控。如果需要的话,还可以通过 GX8002 的 SPI 接口,获取唤醒的 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.h】GX8002 OTA 使用的 bootloader 二进制文件
- 【gxcodec_main.c】驱动主代码
- 【gxcodec_i2c.c】驱动 I2C 代码,包含交互细节
- 【gxcodec_spi.c】驱动 SPI 代码,包含交互细节
- 【gxcodec_upgrade.c】GX8002 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