eclipse输出in thread main_RT-Thread 音频驱动开发

7bc6889ffbf64054fd9cd1264884b1ac.png

今天主要是讲解音频虚拟驱动来分析驱动的编写。但是这篇文章并不会讲解关于 RT-Thread IO Device 框架相关内容,如果有对这部分不太熟悉的人请先看这个链接了解基本概念:

RT-Thread I/O 设备模型

1. RT-Thread 音频框架图

58bcd73ed59549d04b0003b584e3d8ec.png

RT-Thread的音频分成了4个部分,但是我们最关系的是上层提供的api和底层驱动需要实现的ops接口就可以了。

2. 如何使用 Audio 驱动

在写驱动之前,我们首先得知道如何测试自己的驱动对吧!所以这里我们首先了解下 RT-Thread 系统中是如何播放音乐!

#include <rtthread.h>
#include <rtdevice.h>
#include <dfs_posix.h>

#define BUFSZ   1024
#define SOUND_DEVICE_NAME    "sound0"    /* Audio 设备名称 */
static rt_device_t snd_dev;              /* Audio 设备句柄 */

struct RIFF_HEADER_DEF
{
    char riff_id[4];     // 'R','I','F','F'
    uint32_t riff_size;
    char riff_format[4]; // 'W','A','V','E'
};

struct WAVE_FORMAT_DEF
{
    uint16_t FormatTag;
    uint16_t Channels;
    uint32_t SamplesPerSec;
    uint32_t AvgBytesPerSec;
    uint16_t BlockAlign;
    uint16_t BitsPerSample;
};

struct FMT_BLOCK_DEF
{
    char fmt_id[4];    // 'f','m','t',' '
    uint32_t fmt_size;
    struct WAVE_FORMAT_DEF wav_format;
};

struct DATA_BLOCK_DEF
{
    char data_id[4];     // 'R','I','F','F'
    uint32_t data_size;
};

struct wav_info
{
    struct RIFF_HEADER_DEF header;
    struct FMT_BLOCK_DEF   fmt_block;
    struct DATA_BLOCK_DEF  data_block;
};

int wavplay_sample(int argc, char **argv)
{
    int fd = -1;
    uint8_t *buffer = NULL;
    struct wav_info *info = NULL;
    struct rt_audio_caps caps = {0};

    if (argc != 2)
    {
        rt_kprintf("Usage:n");
        rt_kprintf("wavplay_sample song.wavn");
        return 0;
    }

    fd = open(argv[1], O_WRONLY);
    if (fd < 0)
    {
        rt_kprintf("open file failed!n");
        goto __exit;
    }

    buffer = rt_malloc(BUFSZ);
    if (buffer == RT_NULL)
        goto __exit;

    info = (struct wav_info *) rt_malloc(sizeof * info);
    if (info == RT_NULL)
        goto __exit;

    if (read(fd, &(info->header), sizeof(struct RIFF_HEADER_DEF)) <= 0)
        goto __exit;
    if (read(fd, &(info->fmt_block),  sizeof(struct FMT_BLOCK_DEF)) <= 0)
        goto __exit;
    if (read(fd, &(info->data_block), sizeof(struct DATA_BLOCK_DEF)) <= 0)
        goto __exit;

    rt_kprintf("wav information:n");
    rt_kprintf("samplerate %dn", info->fmt_block.wav_format.SamplesPerSec);
    rt_kprintf("channel %dn", info->fmt_block.wav_format.Channels);

    /* 根据设备名称查找 Audio 设备,获取设备句柄 */
    snd_dev = rt_device_find(SOUND_DEVICE_NAME);

    /* 以只写方式打开 Audio 播放设备 */
    rt_device_open(snd_dev, RT_DEVICE_OFLAG_WRONLY);

    /* 设置采样率、通道、采样位数等音频参数信息 */
    caps.main_type               = AUDIO_TYPE_OUTPUT;                           /* 输出类型(播放设备 )*/
    caps.sub_type                = AUDIO_DSP_PARAM;                             /* 设置所有音频参数信息 */
    caps.udata.config.samplerate = info->fmt_block.wav_format.SamplesPerSec;    /* 采样率 */
    caps.udata.config.channels   = info->fmt_block.wav_format.Channels;         /* 采样通道 */
    caps.udata.config.samplebits = 16;                                          /* 采样位数 */
    rt_device_control(snd_dev, AUDIO_CTL_CONFIGURE, &caps);

    while (1)
    {
        int length;

        /* 从文件系统读取 wav 文件的音频数据 */
        length = read(fd, buffer, BUFSZ);

        if (length <= 0)
            break;

        /* 向 Audio 设备写入音频数据 */
        rt_device_write(snd_dev, 0, buffer, length);
    }

    /* 关闭 Audio 设备 */
    rt_device_close(snd_dev);

__exit:

    if (fd >= 0)
        close(fd);

    if (buffer)
        rt_free(buffer);

    if (info)
        rt_free(info);

    return 0;
}
MSH_CMD_EXPORT(wavplay_sample,  play wav file);

这段代码主要是播放 wav(pcm) 的音频。那么我们来分析下上面一段代码,这段播放一段音频数据的主要步骤如下:

  1. #define SOUND_DEVICE_NAME "sound0": 首先定义播放的驱动
  2. fd = open(argv[1], O_WRONLY);: 用于打开音频文件,这个没什么分析的
  3. snd_dev = rt_device_find(SOUND_DEVICE_NAME);: 首先查找 Audio 设备获取设备句柄
  4. rt_device_open(snd_dev, RT_DEVICE_OFLAG_WRONLY);: 以只写方式打开 Audio 设备,也就是打开放音设备
  5. rt_device_control(snd_dev, AUDIO_CTL_CONFIGURE, &caps);: 置音频参数信息(采样率、通道等)
  6. length = read(fd, buffer, BUFSZ);: 读取音频文件的数据
  7. rt_device_write(snd_dev, 0, buffer, length);: 向驱动写入音频文件数据,写入后就会出声音,写入的数据为pcm数据,音频相关格式是步骤5中配置的参数
  8. rt_device_close(snd_dev);: 播放完成,关闭设备

这样看起来是不是非常简单,将这段代码添加到你的代码中进行编译下载,就可以了放音乐了,当然只能播放wav格式的音频。

这个时候肯定有大佬已经反应过来了,我bsp连个audio驱动都没有,脑补音乐吗!大佬不要心急,小弟这就给你把驱动慢慢道来~

3. 编写音频虚拟驱动

上来废话不多说,直接上干货:

#include "drv_sound.h"
#include "drv_tina.h"
#include "drivers/audio.h"

#define DBG_TAG "drv_sound"
#define DBG_LVL DBG_LOG
#define DBG_COLOR
#include <rtdbg.h>

#define TX_DMA_FIFO_SIZE (2048)

struct temp_sound
{
    struct rt_audio_device device;
    struct rt_audio_configure replay_config;
    int volume;
    rt_uint8_t *tx_fifo;
};

static rt_err_t getcaps(struct rt_audio_device *audio, struct rt_audio_caps *caps)
{
    struct temp_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL);
    sound = (struct temp_sound *)audio->parent.user_data; (void)sound;

    return RT_EOK;
}

static rt_err_t configure(struct rt_audio_device *audio, struct rt_audio_caps *caps)
{
    struct temp_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL);
    sound = (struct temp_sound *)audio->parent.user_data; (void)sound;

    return RT_EOK;
}

static rt_err_t init(struct rt_audio_device *audio)
{
    struct temp_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL);
    sound = (struct temp_sound *)audio->parent.user_data; (void)sound;

    return RT_EOK;
}

static rt_err_t start(struct rt_audio_device *audio, int stream)
{
    struct temp_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL);
    sound = (struct temp_sound *)audio->parent.user_data; (void)sound;

    return RT_EOK;
}

static rt_err_t stop(struct rt_audio_device *audio, int stream)
{
    struct temp_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL);
    sound = (struct temp_sound *)audio->parent.user_data; (void)sound;  

    return RT_EOK;
}

rt_size_t transmit(struct rt_audio_device *audio, const void *writeBuf, void *readBuf, rt_size_t size)
{
    struct temp_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL);
    sound = (struct temp_sound *)audio->parent.user_data; (void)sound;

    return size;
}

static void buffer_info(struct rt_audio_device *audio, struct rt_audio_buf_info *info)
{
    struct temp_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL);
    sound = (struct temp_sound *)audio->parent.user_data;

    /**
     *               TX_FIFO
     * +----------------+----------------+
     * |     block1     |     block2     |
     * +----------------+----------------+
     *    block_size  /
     */
    info->buffer      = sound->tx_fifo;
    info->total_size  = TX_DMA_FIFO_SIZE;
    info->block_size  = TX_DMA_FIFO_SIZE / 2;
    info->block_count = 2;
}

static struct rt_audio_ops ops =
{
    .getcaps     = getcaps,
    .configure   = configure,
    .init        = init,
    .start       = start,
    .stop        = stop,
    .transmit    = transmit,
    .buffer_info = buffer_info,
};

static int rt_hw_sound_init(void)
{
    rt_uint8_t *tx_fifo = RT_NULL;
    static struct temp_sound sound = {0};

    /* 分配 DMA 搬运 buffer */
    tx_fifo = rt_calloc(1, TX_DMA_FIFO_SIZE);
    if(tx_fifo == RT_NULL)
    {
        return -RT_ENOMEM;
    }

    sound.tx_fifo = tx_fifo;

    /* 注册声卡放音驱动 */
    sound.device.ops = &ops;
    rt_audio_register(&sound.device, "sound0", RT_DEVICE_FLAG_WRONLY, &sound);

    return RT_EOK;
}
INIT_DEVICE_EXPORT(rt_hw_sound_init);

上面是整个audio驱动的架子,当然没有如何和硬件相关的代码,但是添加到项目中,是可以在shell中使用list_device命令看到 sound0 驱动的。如果我们将第一章中的代码配合的话是可以播放 wav 音频,当然由于没有硬件相关代码是不会出声音的。

我们先来分析下这段代码:

  1. rt_hw_sound_init 函数是驱动的入口,用于注册audio框架,在这个里面,我们分配了 audio dma 需要的buffer,并将 实现的音频相关的ops注册到sound0音频设备中。调用这个函数后就可以在list_device中看到sound0驱动了。
  2. 那么接下来有疑问了struct rt_audio_ops ops这个结构体中的几个函数分别是干什么的如何编写。那么笔者给大家慢慢道来!
  3. 由于 audio 相关的配置和设置的参数比较多,所以这里我们将配置和获取参数分别分成了2个 ops 函数来实现,分别为 getcaps 和 configure。getcaps 用于获取 audio 的能力,例如硬件通道数,当前采样率,采样深度,音量,configure 函数用于实现设置通道数,当前采样率,采样深度,音量。
  4. init ops函数,主要用于实现 芯片的 i2s(与外部codec进行音频数据通信) i2c(控制外部codec的采样率,mute脚,当然部分codec内置的是不需要这个的,还有部分比较低端一点的codec也是不会有i2c控制的,这个根据大家外部接的芯片来确定),当然还需要配置 dma 和 dma 中端。还有控制 mute 的gpio引脚。
  5. start ops 函数主要是用于启动 dma 和 关mute 相关的处理的。
  6. stop ops 函数主要是用于关闭 dma 和 开mute 相关的处理的。
  7. transmit 主要是用于触发数据的搬运,为什么说是触发搬运呢?其实上层代码向音频设备写入音频数据并不会直接写入到驱动中,也就是不会直接调用transmit这个底层函数用于将缓冲区的数据传递到 dma 的buffer中,那么transmit会在什么时候调用呢?上面的驱动并不会触发驱动的搬运也就是这个函数,其实我们可以看到 audio 框架中有一个函数 rt_audio_tx_complete(&sound->device); 这个函数就是用于通知搬运的,那么我们再来梳理下这个段逻辑:
  • 上层应用调用 rt_device_write 函数向 audio 写入数据,框架层会将写入的数据缓存到内部的一个buffer(静态内存池中的一个节点,默认配置为2k数据)
  • 上层写入超过2k的数据会阻塞等待
  • 第一次使用 rt_device_write 会调用 start ops函数启动 dma搬运,在i2s的dma中断(半空和满中断服务函数中)调用 rt_audio_tx_complete 函数
  • rt_audio_tx_complete 表示 dma的 数据搬运完毕了,需要填充下一次的音频数据,这个函数会调用 transmit ops,但是如果是i2s dma循环搬运的数据,dma会自动搬运数据,所以并不需要使用 transmit ops来将音频缓冲区的数据 copy 到驱动的dma中,那么transmit 有什么用呢?第一在部分没有dma循环搬运的芯片上我们可以利用这个函数触发下一个dma搬运或者是cpu搬运,第二这个地方可以用来刷cache的!
  1. buffer_info 用于告诉audio框架你的音频驱动缓冲区有多大,有几块,这样上层通过 transmit ops函数的时候就知道给你多少字节数据了!

看了上面的分析我相信你应该了解了基本原理了,和编写方法了。但是这个驱动还是不能出声音,那么我们得想办法实现一个驱动,由于笔者的硬件和大家都不一样,那么小弟想了一个办法。

那就是将音频缓存到文件中~,这里我们来做一个虚拟音频驱动,这个驱动并不会出声音,但是会将数据保存层pcm文件。pcm的相关参数和你播放的wav一样这样我们可以用电脑来播放了。这样就避免硬件的差异化。

5. 音频虚拟驱动编写

还是废话不多说,直接上代码。

/*
* File: drv_virtual.c
*
* COPYRIGHT (C) 2012-2019, Shanghai Real-Thread Technology Co., Ltd
*/

#include "drv_virtual.h"
#include "dfs.h"
#include "dfs_posix.h"

#define DBG_TAG "drv_virtual"
#define DBG_LVL DBG_LOG
#define DBG_COLOR
#include <rtdbg.h>

#define TX_DMA_FIFO_SIZE (2048)

struct tina_sound
{
    struct rt_audio_device device;
    struct rt_audio_configure replay_config;
    int volume;
    rt_uint8_t *tx_fifo;
    int fd;
    struct rt_thread thread;
    int endflag;
};

static rt_err_t getcaps(struct rt_audio_device *audio, struct rt_audio_caps *caps)
{
    rt_err_t ret = RT_EOK;
    struct tina_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL);
    sound = (struct tina_sound *)audio->parent.user_data; (void)sound;

    switch(caps->main_type)
    {
    case AUDIO_TYPE_QUERY:
    {
        switch (caps->sub_type)
        {
        case AUDIO_TYPE_QUERY:
            caps->udata.mask = AUDIO_TYPE_OUTPUT | AUDIO_TYPE_MIXER;
            break;

        default:
            ret = -RT_ERROR;
            break;
        }

        break;
    }

    case AUDIO_TYPE_OUTPUT:
    {
        switch(caps->sub_type)
        {
        case AUDIO_DSP_PARAM:
            caps->udata.config.channels   = sound->replay_config.channels;
            caps->udata.config.samplebits = sound->replay_config.samplebits;
            caps->udata.config.samplerate = sound->replay_config.samplerate;
            break;

        default:
            ret = -RT_ERROR;
            break;
        }

        break;
    }

    case AUDIO_TYPE_MIXER:
    {
        switch (caps->sub_type)
        {
        case AUDIO_MIXER_QUERY:
            caps->udata.mask = AUDIO_MIXER_VOLUME | AUDIO_MIXER_LINE;
            break;

        case AUDIO_MIXER_VOLUME:
            caps->udata.value = sound->volume;
            break;

        case AUDIO_MIXER_LINE:
            break;

        default:
            ret = -RT_ERROR;
            break;
        }

        break;
    }

    default:
        ret = -RT_ERROR;
        break;
    }

    return ret;
}

static rt_err_t configure(struct rt_audio_device *audio, struct rt_audio_caps *caps)
{
    rt_err_t ret = RT_EOK;
    struct tina_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL);
    sound = (struct tina_sound *)audio->parent.user_data; (void)sound;

    switch(caps->main_type)
    {
    case AUDIO_TYPE_MIXER:
    {
        switch(caps->sub_type)
        {
        case AUDIO_MIXER_VOLUME:
        {
            int volume = caps->udata.value;
            sound->volume = volume;
            break;
        }

        default:
            ret = -RT_ERROR;
            break;
        }

        break;
    }

    case AUDIO_TYPE_OUTPUT:
    {
        switch(caps->sub_type)
        {
        case AUDIO_DSP_PARAM:
        {
            int samplerate;

            samplerate = caps->udata.config.samplerate;
            sound->replay_config.samplerate = samplerate;
            LOG_I("set samplerate = %d", samplerate);
            break;
        }

        case AUDIO_DSP_SAMPLERATE:
        {
            int samplerate;

            samplerate = caps->udata.config.samplerate;
            sound->replay_config.samplerate = samplerate;
            LOG_I("set samplerate = %d", samplerate);
            break;
        }

        case AUDIO_DSP_CHANNELS:
        {
            break;
        }

        default:
            break;
        }

        break;
    }

    default:
        break;
    }

    return ret;
}

static void virtualplay(void *p)
{
    struct tina_sound *sound = (struct tina_sound *)p; (void)sound;

    while(1)
    {
        /* tick = TX_DMA_FIFO_SIZE/2 * 1000ms / 44100 / 4 ≈ 5.8 */
        rt_thread_mdelay(6);
        rt_audio_tx_complete(&sound->device);

        if(sound->endflag == 1)
        {
            break;
        }
    }
}

static int thread_stack[1024] = {0};

static rt_err_t init(struct rt_audio_device *audio)
{
    struct tina_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL);
    sound = (struct tina_sound *)audio->parent.user_data; (void)sound;

    LOG_I("sound init");

    return RT_EOK;
}

static rt_err_t start(struct rt_audio_device *audio, int stream)
{
    struct tina_sound *sound = RT_NULL;
    rt_err_t ret = RT_EOK;

    RT_ASSERT(audio != RT_NULL);
    sound = (struct tina_sound *)audio->parent.user_data; (void)sound;

    LOG_I("sound start");

    ret = rt_thread_init(&sound->thread, "virtual", virtualplay, sound, &thread_stack, sizeof(thread_stack), 1, 10);
    if(ret != RT_EOK)
    {
        LOG_E("virtual play thread init failed");
        return (-RT_ERROR);
    }
    rt_thread_startup(&sound->thread);

    sound->endflag = 0;

    sound->fd = open("/tmp/virtual.pcm", O_CREAT | O_RDWR, 0666);

    return RT_EOK;
}

static rt_err_t stop(struct rt_audio_device *audio, int stream)
{
    struct tina_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL);
    sound = (struct tina_sound *)audio->parent.user_data; (void)sound;

    LOG_I("sound stop");  

    sound->endflag = 1;

    close(sound->fd);
    sound->fd = -1;

    return RT_EOK;
}

rt_size_t transmit(struct rt_audio_device *audio, const void *wb, void *rb, rt_size_t size)
{
    struct tina_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL);
    sound = (struct tina_sound *)audio->parent.user_data; (void)sound;

    return write(sound->fd, wb, size);
}

static void buffer_info(struct rt_audio_device *audio, struct rt_audio_buf_info *info)
{
    struct tina_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL);
    sound = (struct tina_sound *)audio->parent.user_data;

    /**
     *               TX_FIFO
     * +----------------+----------------+
     * |     block1     |     block2     |
     * +----------------+----------------+
     *    block_size  /
     */
    info->buffer      = sound->tx_fifo;
    info->total_size  = TX_DMA_FIFO_SIZE;
    info->block_size  = TX_DMA_FIFO_SIZE / 2;
    info->block_count = 2;
}

static struct rt_audio_ops ops =
{
    .getcaps     = getcaps,
    .configure   = configure,
    .init        = init,
    .start       = start,
    .stop        = stop,
    .transmit    = transmit,
    .buffer_info = buffer_info,
};

static int rt_hw_sound_init(void)
{
    rt_uint8_t *tx_fifo = RT_NULL;
    static struct tina_sound sound = {0};

    /* 分配 DMA 搬运 buffer */
    tx_fifo = rt_calloc(1, TX_DMA_FIFO_SIZE);
    if(tx_fifo == RT_NULL)
    {
        return -RT_ENOMEM;
    }

    sound.tx_fifo = tx_fifo;

    /* 配置 DSP 参数 */
    {
        sound.replay_config.samplerate = 44100;
        sound.replay_config.channels   = 2;
        sound.replay_config.samplebits = 16;
        sound.volume                   = 60;
        sound.fd                       = -1;
        sound.endflag                  = 0;
    }

    /* 注册声卡放音驱动 */
    sound.device.ops = &ops;
    rt_audio_register(&sound.device, "sound0", RT_DEVICE_FLAG_WRONLY, &sound);

    return RT_EOK;
}
INIT_DEVICE_EXPORT(rt_hw_sound_init);

根据第二部分的分析,相信你也能看懂这部分代码,这个驱动的根本思想是利用 virtualplay 线程模拟 i2s dma进行数据的自动搬运!!!!

最终文件会保存到 /tmp/virtual.pcm 中,注意这里有点是 virtualplay 函数延时了6ms是为了模拟dma buffer中 1k 数据搬运(播放)需要消耗的时间,tick = TX_DMA_FIFO_SIZE/2 * 1000ms / 44100 / 4 ≈ 5.8ms 。所以我们得要求文件写入比较快,这里笔者利用了ramfs来实现文件系统,经过实际测试如果写入sd卡或者flash会非常的慢,所以还是建议使用 ramfs 保证 20Mbytes 以上的大小,当然可以使用 qemu 来测试~~~


那么小弟就分析到这里,更加多的信息请加入 qq 群 690181735 讨论,有更多更专业RT-Thread audio相关资料等着你,构建生态和资料才是更好的发展!!!-

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值