数字化语音采集回放系统

前段时间,和小伙伴们一起基于STMF407的核心板,做了一个录音装置。便来回顾一下这个过程,针对过程中的问题,做点笔记。
以下是录音装置基本要求:

1)语音信号滤波器:通带为300Hz3.4kHz
2ADC:采样频率f≥16kHz,字长12位;
3)语音存储时间≥20秒;
4DAC:变换频率f≥16kHz,字长12位;
5)回放语音质量良好。
6增加音量和播放控制功能;
 7)二级菜单界面,并显示播放和录音时间。

我们遇到的第一个问题是关于F407片内Flash的写入速度。写入太慢,采样率低且可能出现断续录音。
起初呢,我们一直用的正点原子探索者F407的库,一直也用的挺好。但由于库对于Flash的读写,正点原子为了保证每次都可以写到任意一个块(4字节的空间),所以在每次写入之前都会计算并擦除一次块所在的扇区,浪费了很多时间。(NandFlash 数据写入时只能写0,不能写1 所以写入前须擦除,保证写入前的位都为1。而且NandFlash擦除一次要擦一个扇区,读取一次读取一个块字节(此处是4个字节))。
  •   库函数里的写入前擦除操作
//从指定地址开始写入指定长度的数据
//特别注意:因为STM32F4的扇区实在太大,没办法本地保存扇区数据,所以本函数
//         写地址如果非0XFF,那么会先擦除整个扇区且不保存扇区数据.所以
//         写非0XFF的地址,将导致整个扇区数据丢失.建议写之前确保扇区里
//         没有重要数据,最好是整个扇区先擦除了,然后慢慢往后写.
//该函数对OTP区域也有效!可以用来写OTP区!
//OTP区域地址范围:0X1FFF7800~0X1FFF7A0F
//WriteAddr:起始地址(此地址必须为4的倍数!!)
//pBuffer:数据指针
//NumToWrite:字(32位)数(就是要写入的32位数据的个数.)
void STMFLASH_Write(u32 WriteAddr,u32 *pBuffer,u32 NumToWrite)    
{
    ....... 
    addrx=WriteAddr;                //写入的起始地址
    endaddr=WriteAddr+NumToWrite*4;    //写入的结束地址
    if(addrx<0X1FFF0000)            //只有主存储区,才需要执行擦除操作!!
    {
        while(addrx<endaddr)        //扫清一切障碍.(对非FFFFFFFF的地方,先擦除)
        {
            if(STMFLASH_ReadWord(addrx)!=0XFFFFFFFF)//有非0XFFFFFFFF的地方,要擦除这个扇区
            {   
                status=FLASH_EraseSector(STMFLASH_GetFlashSector(addrx),VoltageRange_3);//VCC=2.7~3.6V之间!!
                if(status!=FLASH_COMPLETE)break;    //发生错误了
            }else addrx+=4;
        }
    }
    .......
}

//获取某个地址所在的flash扇区
//addr:flash地址
//返回值:0~11,即addr所在的扇区
uint16_t STMFLASH_GetFlashSector(u32 addr)
{
    if(addr<ADDR_FLASH_SECTOR_1)return FLASH_Sector_0;
    else if(addr<ADDR_FLASH_SECTOR_2)return FLASH_Sector_1;
    else if(addr<ADDR_FLASH_SECTOR_3)return FLASH_Sector_2;
    else if(addr<ADDR_FLASH_SECTOR_4)return FLASH_Sector_3;
    else if(addr<ADDR_FLASH_SECTOR_5)return FLASH_Sector_4;
    else if(addr<ADDR_FLASH_SECTOR_6)return FLASH_Sector_5;
    else if(addr<ADDR_FLASH_SECTOR_7)return FLASH_Sector_6;
    else if(addr<ADDR_FLASH_SECTOR_8)return FLASH_Sector_7;
    else if(addr<ADDR_FLASH_SECTOR_9)return FLASH_Sector_8;
    else if(addr<ADDR_FLASH_SECTOR_10)return FLASH_Sector_9;
    else if(addr<ADDR_FLASH_SECTOR_11)return FLASH_Sector_10;
    return FLASH_Sector_11;    
}
为了解决这种写入慢的问题,我们采用录音前初始化Flash的方式,将擦除时间集中在数据采集前,保证写入过程时无需擦除,提高写入速度。
Flash部分代码过程如下:
//Flash 扇区擦除,屏蔽部分是避免擦除前3个扇区,导致用户代码也被擦除。可以根据生成的Hex文件大小而定
u8 STMFLASH_erase(u8 lock_flag)//lock_flag 上锁标志,为真上锁
{
    FLASH_Status status = FLASH_COMPLETE;
    FLASH_Unlock();    //解锁
    FLASH_DataCacheCmd(DISABLE);//FLASH擦除期间,必须禁止数据缓存
//    status = FLASH_EraseSector(FLASH_Sector_0, VoltageRange_3); //VCC=2.7~3.6V之间!!
//    if(status != FLASH_COMPLETE)  return 1;    //发生错误了  
//    status = FLASH_EraseSector(FLASH_Sector_1, VoltageRange_3); //VCC=2.7~3.6V之间!!
//    if(status != FLASH_COMPLETE)  return 1;    //发生错误了
//    status = FLASH_EraseSector(FLASH_Sector_2, VoltageRange_3); //VCC=2.7~3.6V之间!!
//    if(status != FLASH_COMPLETE)  return 1;    //发生错误了 

    status = FLASH_EraseSector(FLASH_Sector_3, VoltageRange_3); //VCC=2.7~3.6V之间!!
    if(status != FLASH_COMPLETE)  return 1;    //发生错误了
    status = FLASH_EraseSector(FLASH_Sector_11, VoltageRange_3); //VCC=2.7~3.6V之间!!
    if(status != FLASH_COMPLETE)  return 1;    //发生错误了
    status = FLASH_EraseSector(FLASH_Sector_4, VoltageRange_3); //VCC=2.7~3.6V之间!!
    if(status != FLASH_COMPLETE)  return 1;    //发生错误了
    status = FLASH_EraseSector(FLASH_Sector_5, VoltageRange_3); //VCC=2.7~3.6V之间!!
    if(status != FLASH_COMPLETE)  return 1;    //发生错误了
    status = FLASH_EraseSector(FLASH_Sector_6, VoltageRange_3); //VCC=2.7~3.6V之间!!
    if(status != FLASH_COMPLETE)  return 1;    //发生错误了
    status = FLASH_EraseSector(FLASH_Sector_7, VoltageRange_3); //VCC=2.7~3.6V之间!!
    if(status != FLASH_COMPLETE)  return 1;    //发生错误了
    status = FLASH_EraseSector(FLASH_Sector_8, VoltageRange_3); //VCC=2.7~3.6V之间!!
    if(status != FLASH_COMPLETE)  return 1;    //发生错误了
    status = FLASH_EraseSector(FLASH_Sector_9, VoltageRange_3); //VCC=2.7~3.6V之间!!
    if(status != FLASH_COMPLETE)  return 1;    //发生错误了
    status = FLASH_EraseSector(FLASH_Sector_10, VoltageRange_3); //VCC=2.7~3.6V之间!!
    if(status != FLASH_COMPLETE)  return 1;    //发生错误了
    status = FLASH_EraseSector(FLASH_Sector_11, VoltageRange_3); //VCC=2.7~3.6V之间!!
    if(status != FLASH_COMPLETE)  return 1;    //发生错误了
    FLASH_DataCacheCmd(ENABLE);    //FLASH擦除结束,开启数据缓存
    if(lock_flag) FLASH_Lock(); //上锁
    return 0;
}

//数据写入函数
int STMFLASH_Write(u32 WriteAddr, u32 *pBuffer, u32 NumToWrite)
{
    u8  Flag = 0;
    u32 ppBuffer = 0;
    FLASH_Status status = FLASH_COMPLETE;
    
    u32 startaddr = Romaddr + WriteAddr;
    endaddr = startaddr + NumToWrite * 4;    //写入的结束地址
    
    if(startaddr < STM32_FLASH_BASE || startaddr % 4) return 1;    //非法地址
    if(endaddr >= 0x080fffff)   {WADDR_staus = full ; return  2;}
    FLASH_Unlock();                    //解锁
    FLASH_DataCacheCmd(DISABLE);//FLASH擦除期间,必须禁止数据缓存
    if(status == FLASH_COMPLETE)
    {
        while(WriteAddr+Romaddr < endaddr) //写数据
        {
            if(FLASH_ProgramWord(WriteAddr+Romaddr, *(pBuffer+ppBuffer)) != FLASH_COMPLETE) //写入数据
            {
                Flag = 1;
                break;    //写入异常
            }
            Romaddr += 4;
            ppBuffer++;
            ppBuffer = ppBuffer%NumToWrite;
        }
    }
    FLASH_Lock();//上锁
    if(Flag) return 3;
    else return 0;
}

效果对比:
修改前
写入100个32bit数据,耗时:218ms.
修改后
Flash  擦除扇区 耗时约10s
写入100个32bit数据,耗时:1.48ms    14.8us/32bits

  • 第二个问题是尖端脉冲
     数字化语音采集回放系统数字化语音采集回放系统
    
    如图,每隔10ms会间隔出现一个尖端脉冲现象。
  
问题分析:可能是由于程序中开了两个定时器,定时器的中断级别造成的。
定时器4 用于控制录音和播放的采样率,为达到16kHz的采样率,定时间隔60us
定时器5 用于控制播放和录音时间,并刷新界面。
    TIM4_Init(60 - 1, 84 - 1);//84M 60us

    TIM5_Init(1000-1, 840 - 1);//84M 10ms
调整了定时器级别如下:
void TIM3_Init(u16 arr, u16 psc)
{
    .......
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00;
    .......
}
void TIM5_Init(u16 arr, u16 psc)
{
    .......
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x01;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;
    .......
}
问题依然存在;
猜想可能是因为定时器冲突。
解决:采用屏蔽定时器5的方式,只采用定时器3.   控制时间对60us进行计数得到。但问题依然未解决。
于是直接屏蔽了时间显示模块,问题解决,
猜想时间模块出现问题,逐渐屏蔽发现
void time_show(void)
{
      Data_ShowNum(90,200,time_s,Red,White,4);
      LCD_ShowString(110,200,":",Black,White);
      Data_ShowNum(120,200,time_ms,Red,White,4);
      LCD_ShowString(150,200,"  s",Black,White);    
}
此函数中的Data_ShowNum();屏蔽后问题消失,得出Data_ShowNum()函数有问题;函数内部如下
void Data_ShowNum(u8 x, u8 y, u32 Num, u16 PenColor, u16 BackColor, u8 P_Index)
{
    u8 Num_Temp[15], i = 1, j = 0;
    u8 count, x_state;
    u8 text[9];
    text[0] = 0X2D;
    count = Data_Len(Num);
    while(i <= count)
    {
        Num_Temp[i] = Num % 10 + 48;
        Num /= 10;
        i++;
    }
    i--;
    x_state = x + 10 * P_Index;
    while(i)
    {
        if(PenColor == Black | P_Index != j)
            LCD_ShowChar(x, y, Num_Temp[i], 16, PenColor, BackColor);
        if(PenColor == Blue)
            LCD_ShowCharString(x_state, y, &text[0], White, PenColor);
        x += 10;
        j++;
        i--;
    }
}
由于未发现Data_ShowNum函数与10ms的关系,无法定位问题。
所以我们换了显示函数,采取如下显示的方式
void time_show(void)
{
    u8 display_s[10];
    u8 display_ms[10];
    sprintf(display_s,"%d",time_s);
    LCD_ShowString(90,200,display_s,Black,White);
    LCD_ShowString(110,200,":",Black,White);
    sprintf(display_ms,"%d",time_ms);
    LCD_ShowString(120,200,display_ms,Black,White); 
    LCD_ShowString(150,200,"  s",Black,White);
//    Data_ShowNum(90,200,time_min,Red,White,4);
//    LCD_ShowString(110,200,":",Black,White);
//    Data_ShowNum(90,200,time_s,Red,White,4);
//    Data_ShowNum(120,200,time_ms,Red,White,4);
}
问题得到解决;波形如下
数字化语音采集回放系统

对于Data_ShowNum()函数的原因,还在查找中,找到后会继续补充。
完!