音乐播放器之在RTT下AUDIO驱动实现(一)

一、前言

其实这件事情的起因还是上半年一次巧合的机会在rtt的论坛看到官方发起的一个关于国民技术开发版测评的活动。巧合的是,在这之前我实现了一个公网对讲机的项目,于是乎头脑一热就报名了。对当时我而言,有几处难点: rtt的驱动框架不熟悉,在mcu下测试i2s接口,以及最后添加外设驱动。名都报了那就硬着头皮往下啃哪里不会学哪里。但是今年又经历了搬家、换城市、找房子、找工作...鸽到现在才开始复盘。

二、介绍

2.1 功能

(本文以ST系列为例)目前主要实现了

  • drv_mic、drv_sound驱动的编写
  • 外设驱动的添加(即直接可以在ENV下进行配置就能生产相关代码)
  • 驱动的测试(回声demo)

2.2 方案 

分两步走:

  1. 首先先在纯裸机下进行实现
    所谓进行回声测试 即将mic收到数据直接发送给功放让其播放出来 这样驱动的功能就得以验证。 但问题是如何流畅的使音频进行输出?我们知道音频流的速度是很快的,所以我们要尽可能的保证数据采集的连续性,这里我们采用DMA双缓存的方式 即一边收集数据同时一边处理,可能会由于收发之间处理的速度不一致导致丢失一部分数据 这对人耳来说可以忽略。以8k采样率 单声道 采样位数16B来说,连续采样2s也才(8000 Hz × 1 × 16 bit × 2 s) /8 = 32000B
    那什么事DMA双缓冲呢
    普通DMA的话目标区域只有一个, 数据存满后 新的数据又来 覆盖旧的数据





    而双缓存的话,DMA的传输目标区域有两个(其实可以把一块内存拆成两部分) 一部分传输结束后会要切换指向两外一个内存区域 即buffer0和buffer1来回切换。这样就能一边收集数据一边把音频数据给到功放进行处理

    而对于STM32F103来说,不支持DMA双缓存就需要我们手动进行切换内存区域 可以使用半中断和全段
    uint8_t buffer0[BufferSize] = {0};
    uint8_t buffer1[BufferSize] = {0};
    uint8_t *Pbuf;
    
    uint16_t t = 0, p = 0;
    //半中断回调
    void HAL_I2S_RxHalfCpltCallback(I2S_HandleTypeDef *hi2s)
    {
      //乒
      Pbuf = &buffer0[0];
      for(p = 0; p < BufferSize/2; p++)
      {
        buffer1[t++] = Pbuf[p];
      }
      p = 0;
      t = BufferSize/2;
    }
    
    //全中断回调
    void HAL_I2S_RxCpltCallback(I2S_HandleTypeDef *hi2s)
    {
      //乓
      static uint8_t i = 1;
      
    //  Pbuf = &buffer0[BufferSize/2];
      if(i==1)
      {         
        i = 0;  
        //开启DMA传输    
        HAL_I2S_Transmit_DMA(&hi2s3, (uint16_t *)buffer1, BufferSize/2);
      }
      Pbuf = &buffer0[BufferSize/2];
      for(p = BufferSize/2; p < BufferSize; p++)
      {
        buffer1[t++] = Pbuf[p];
      }
      p = t = 0;
    }
  2. 裸机下测试通过后那就上操作系统
    在分析之前我们先来回顾下RTT的设备驱动框架 下面是官方文档
    rtt-thread设备驱动框架
    1) 引入设备驱动的话 相当于有一套统一的接口提供给开发者使用。每个厂家的硬件接口都是不样的 那RTT是怎么将接口进行统一?这里RTT提供了一套设备驱动框架 分为三层:I/O设备管理层、设备驱动框架层、设备驱动层

    (具体每一层含义可以参考下上面的链接文档) 从下往上依次简单介绍下:设备驱动层其实就是对应drv_ 这类直接直接驱动设备工作或者说访问硬件功能的程序 笔者这里的工作主要完成了drv_mic以及drv_sound;设备驱动框架层其实就是对应的 以音频驱动框架来说就是audio.c或者说串口的serial.c 这一层主要事先把接口给定死 后面要使用的人必须按照这一套来,根据RTT这套规则来注册 定义;I/O设备管理层主要对应的是device.c这部分 其实rt_device是属于设备的基类,每个设备对象(UART、AUDIO、PIN)都是有父类(基类)派生出来的, 每个具体设备都可以继承其父类对象的属性。
     2) 上面也说了 设备驱动框架层会事先定死接口,你要将设备添加进来必须要遵守这套接口,设备框架框架层里面是不关心你底层如何实现的 只要按照给定的接口注册进来就好。 在audio.h中会提供一个rt_audio_ops

    struct rt_audio_ops
    {
        rt_err_t (*getcaps)(struct rt_audio_device *audio, struct rt_audio_caps *caps);
        rt_err_t (*configure)(struct rt_audio_device *audio, struct rt_audio_caps *caps);
        rt_err_t (*init)(struct rt_audio_device *audio);
        rt_err_t (*start)(struct rt_audio_device *audio, int stream);
        rt_err_t (*stop)(struct rt_audio_device *audio, int stream);
        rt_ssize_t (*transmit)(struct rt_audio_device *audio, const void *writeBuf, void *readBuf, rt_size_t size);
        /* get page size of codec or private buffer's info */
        void (*buffer_info)(struct rt_audio_device *audio, struct rt_audio_buf_info *info);
    };

    我们就需要根据这提供的函数指针实现底层设备的对接 最后完成注册。 以mic设备为例 作者这边使用的是Inmp441 i2s是标准的飞利浦协议 首先先定义一个这样的结构体

    struct stm32_mic
    {
      struct rt_audio_device audio;
        struct rt_audio_configure config; 
      rt_uint8_t *mic_pbuf;
    };
    
    static struct stm32_mic _stm32_mic_drv;

    目前这边只是先实现最基本功能 所以只完成了初始化 开始 停止这几个功能 (先够能跑起来就行)

    static rt_err_t stm32_mic_init(struct rt_audio_device *audio)
    {
      rt_err_t result = RT_EOK;
      
      Inmp441_Init();
    //  HAL_I2S_MspInit(&hi2s2);
      rt_kprintf("drv_mic init success\r\n");
      
      return result;
    }
    
    static rt_err_t stm32_mic_start(struct rt_audio_device *audio, int stream)
    {
      rt_err_t result = RT_EOK;
      
      if(stream == AUDIO_STREAM_RECORD)
      {
        //启动DMA接收
        HAL_I2S_Receive_DMA(&hi2s2, (rt_uint16_t *)_stm32_mic_drv.mic_pbuf, RX_FIFO_SIZE/2);
        rt_kprintf("drv_mic start work\r\n");
      }
      
      return result;
    }
    
    static rt_err_t stm32_mic_stop(struct rt_audio_device *audio, int stream)
    {
      rt_err_t result = RT_EOK;
      
      if(stream == AUDIO_STREAM_RECORD)
      {
        //停止DMA接收
        HAL_I2S_DMAStop(&hi2s2);
        rt_kprintf("drv_mic stop work\r\n");
      }
      
      return result;
    }

    最后完成结构体的初始化以及mic的注册

    static struct rt_audio_ops _mic_audio_ops =
    {
        .getcaps     = RT_NULL,
        .configure   = RT_NULL,
        .init        = stm32_mic_init,
        .start       = stm32_mic_start,
        .stop        = stm32_mic_stop,
        .transmit    = RT_NULL,
        .buffer_info = RT_NULL,
    };
    
    int rt_hw_mic_init()
    {
      rt_uint8_t *rx_fifo = NULL;
      
      rx_fifo = rt_malloc(RX_FIFO_SIZE);
      if(rx_fifo == NULL)
        return -RT_ENOMEM;
      //数组初始化
      rt_memset(rx_fifo, 0, (RX_FIFO_SIZE));
      //赋值
      _stm32_mic_drv.mic_pbuf = rx_fifo;
      
      _stm32_mic_drv.audio.ops = &_mic_audio_ops;
      //注册驱动
      rt_audio_register(&_stm32_mic_drv.audio,
                        "mic0",
                        RT_DEVICE_FLAG_RDONLY,
                        &_stm32_mic_drv);
      
      return RT_EOK;
    }
    
    INIT_DEVICE_EXPORT(rt_hw_mic_init);

    这样其实就完成了最基本的功能,由于本方案中需要使用到DMA双缓存所以还继续完成半中断和全中断部分内容这里暂时不展开说了。接下来我们再一层层的剥开代码研究下提供对外的应用层代码是如何控制底层的以及audio.c里面这些函数相互作用
    3) rt_hw_mic_init函数中我们完成对设备驱动的注册rt_audio_register,在这个函数中其实主要是事先写好了相应的初始化、打开、关闭、读写等函数。

    对于rt_device_open

    rt_device_open
        device_init(dev) <<==>> device->init = _audio_dev_init;
                audio->ops->init(audio) <<==>> stm32_mic_init
    

    应用层通过使用rt_device_open 函数其实是这样一步步调用到我们之前写的硬件设备的初始化函数 (这里面其实还做了一步打开设备的工作 device_open(dev, oflag);)

    rt_device_read 是标准的设备接口读音频设备

    rt_device_read
        device_read(dev, pos, buffer, size) <<==>> device->read = _audio_dev_read;
            rt_device_read(RT_DEVICE(&audio->record->pipe), pos, buffer, size);
                最终会到audio_pip 的rt_pipe_read中
    

    这个函数主要读取record->pipe设备,也就是从pipe的ringbuffer里读取数据 那么问题来了ringbuffer里的数据从哪来的?那既然是录音 那应该就由硬件设备接收到的;那什么时候接收到的呢?那这时候就需要在DMA中断里面进行处理了 在中断中调用rt_audio_rx_done函数 该函数可以完成向record->pipe写入数据。 由于本方案采用的是DMA双缓存 那在中断中就会来回轮询的向两个buffer写入数据啦。至此drv_mic.c部分就分析完了,那drv_sound.c其实主要差不多唯一区别是sound部分是发送数据进行REPLAY

    rt_device_write 是标准设备接口写音频设备

    rt_device_write
        device_write(dev, pos, buffer, size) <<==>> device->write = _audio_dev_write;
            _aduio_replay_start(audio);
                audio->ops->start(audio, AUDIO_STREAM_REPLAY) <<==>> stm32_sound_start
    

    最终会调用到底层驱动函数stm32_sound_start 进行一个开启DMA传输的功能 将数据发送出去。_audio_dev_write函数主要功能将要写入的数据按照replay内存池中的内存块大小进行分割 满足内存块大小的部分当做数据队列写入replay->write_data,在DMA中断中调用rt_audio_tx_complete开始真正的数据发送(实际是调用的_audio_send_replay_frame)
    4) 最后来看下整体的一个驱动框架

  3. 2.3 外设驱动添加

         可以先看下官方文档BSP制作部分的外设驱动添加指南
          rtt-thread设备驱动框架
          以及Kconfig语法的使用
          Kconfig
          最后上一下效果
          
          

     OK暂时先记录到这里,下一篇很快会来哟!周末愉快!

  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
使用RTT(实时操作系统)来驱动小车运动可以分为以下几个步骤: 1. 配置硬件:首先,需要将RTT与小车硬件进行连接。连接方式包括串口、I2C、SPI等常见的接口。根据小车硬件的接口特性和RTT的支持情况,选择合适的接口进行连接。 2. 编写驱动程序:在RTT中编写小车驱动程序,将控制小车运动的相关操作封装为函数或任务。根据小车硬件的控制方式,实现前进、后退、左转、右转等基本运动功能。同时,可以添加其他功能如速度控制、传感器读取等。 3. 创建任务:使用RTT的任务管理功能,为小车驱动程序创建一个任务。任务是一个独立运行的线程,负责调用小车驱动程序,控制小车的运动。可以根据具体需求设置任务的优先级和调度周期。 4. 编译和烧录:将编写好的RTT程序编译生成可执行文件,然后将其烧录到嵌入式系统中。烧录方式取决于硬件平台,可以使用编程器、调试器或通过网络进行烧录。 5. 运行:将烧录好的程序加载到嵌入式系统中,并启动RTTRTT会自动创建任务并开始执行。小车驱动程序会在任务中被调用,从而控制小车的运动。 通过以上步骤,就可以使用RTT驱动小车运动。使用RTT的优势是能够实现实时性要求高的控制任务,提供了任务管理和调度功能,使得驱动程序能够按照要求的周期进行运行。同时,RTT还支持多任务、优先级调度等功能,能够灵活应对不同的应用场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值