一、前言
其实这件事情的起因还是上半年一次巧合的机会在rtt的论坛看到官方发起的一个关于国民技术开发版测评的活动。巧合的是,在这之前我实现了一个公网对讲机的项目,于是乎头脑一热就报名了。对当时我而言,有几处难点: rtt的驱动框架不熟悉,在mcu下测试i2s接口,以及最后添加外设驱动。名都报了那就硬着头皮往下啃哪里不会学哪里。但是今年又经历了搬家、换城市、找房子、找工作...鸽到现在才开始复盘。
二、介绍
2.1 功能
(本文以ST系列为例)目前主要实现了
- drv_mic、drv_sound驱动的编写
- 外设驱动的添加(即直接可以在ENV下进行配置就能生产相关代码)
- 驱动的测试(回声demo)
2.2 方案
分两步走:
- 首先先在纯裸机下进行实现
所谓进行回声测试 即将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; }
-
裸机下测试通过后那就上操作系统
在分析之前我们先来回顾下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_opsstruct 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_openrt_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) 最后来看下整体的一个驱动框架 -
2.3 外设驱动添加
可以先看下官方文档BSP制作部分的外设驱动添加指南
rtt-thread设备驱动框架
以及Kconfig语法的使用
Kconfig
最后上一下效果
OK暂时先记录到这里,下一篇很快会来哟!周末愉快!