作者:Catherine


语音识别与签到系统

近年来,语音识别在语音导航,室内设备控制。人际对话等方面得到了广泛的应用。

我们在今年第1期杂志《为设备加入社交网络功能》中,实现了W5500EVB自己发微博功能。试想假设我们把语音识别与微博签到结合起来,我们上班时,报上姓名。经识别后,摄像头为我们拍张照片。传到新浪微博,这样既能得到我们签到的时间,又能保证是本人签到,可靠高效,同一时候朋友通过微博能了解到我们上班时的状态,这样是不是非常有意思呢?

今天要介绍的就是上面提到的,基于语音识别的微博签到系统。我们用摄像头ov2640拍照,LD3320做语音识别。然后W5500EVB把我们想说的话,以及照片发送到新浪微博。


基于语音识别的微博签到系统设计

(1)   开发板基本情况

a)        单片机:STM32F103RCT6,256K字节Flash。48K字节SRAM,2K字节EEPROM

b)       以太网控制器:W5500,SPI接口与单片机相连

c)        电源:USB供电

(2)   开发工具: IARfor ARM v5.41,这是我们project所使用的版本号。假设使用不同版本号的IAR。请对STM的库稍作调整。

(3)   语音识别:LD3320语音识别模块。

(4)   图像生成:OV2640摄像头。

(5)   其它

a)        新浪微博username和password;如若没有,就赶快给你的设备申请一个吧。

b)       一根Mini接口的USB线,如图1所看到的。

c)        一根网线。

d)       STM32芯片的串口程序烧录工具,STM官方提供的程序名为:Flash Loader Demo。

 

图1系统实物图

首先,我们了解一下整个程序流程,流程图由一个主流程图(见图2)和四个子流程图(图3,图4,图5。图6)组成。在STM32及ov2640初始化完毕之后,将进行网络參数配置,依据自己网络的情况配置W5500的IP地址等网络參数,确保W5500能连接外网。

然后配置LD3320语音模块,语音模块处于初始状态,将进行写入识别列表,启动语音识别过程,当我们对着麦克风说话的时候,LD3320检測到有语音输入,LD3320将进入中断,在中断中将把我们说的内容与寄存器里的词条比較,假设找到1-4个候选答案。返回“找到识别结果”状态,假设没有找到候选答案。返回“未找到识别结果”状态。

在下一次循环中,LD3320假设是“找到识别结果”状态。将拍摄照片及发送微博。假设是“未找到识别结果”状态。将进入初始状态。假设是“正在识别”或者“识别错误”将又一次检查LD3320的状态。各个子流程图描写叙述的比較详尽。这里不再一一赘述。

对于拍摄照片子流程图。我们须要了解jpg图片的数据格式。图片的前两个字节是0xff,0xd8,最后两个字节是0xff。0xd9,在中断程序接收图片数据的过程中,首先推断数据是不是前两个字节。假设是。保存数据,后面的数据是先保存。然后推断是不是数据结尾。直到接收成功。

 


LD3320介绍

1 通过高速而稳定的优化算法,完毕非特定人语音识别。识别准确率95%。

2 不须要外接不论什么辅助的Flash芯片。RAM芯片和AD芯片,就能够完毕语音识别功能。

3 每次识别最多能够设置50项候选识别句,每一个识别句能够是单字。词组或短句。长度为不超过10个汉字或者79个字节的拼音串。识别句内容还能够动态编辑改动。

4 芯片内部已经准备了16位A/D转换器、16位D/A转换器和功放电路,麦克风、立体声耳机和单声道喇叭能够非常方便地和芯片管脚连接。

5 支持并行和串行接口,串行方式能够简化与其它模块的连接。

在本系统中採用的LD3320模块如图7,LD3320芯片外部已经连接了麦克风。耳机接口,基本电路,仅仅引出了我们须要的引脚。本系统採用串行方式,串行接口通过SPI协议和外部主CPU连接,首先要将MD接高电平。将SPIS接地,选定LD3320工作在串行模式,此时使用的管脚有:片选(SCS*)、SPI时钟(SDCK)、SPI输入(SDI)和SPI输出(SDO),中断引脚(INT),复位引脚(RST),时钟引脚(CLK)。通过SPI接口。配置LD3320的工作模式。读取识别结果,图8,图9为SPI读写时序。

当LD3320识别到有语音输入。INT引脚将产生中断,在中断处理函数中。读取识别结果。改变LD3320状态。

 

在本系统中,OV2640输出JPEG压缩图像格式。MCU与OV2640的通信採用串行与并行结合。OV2640带有SCCB(Serial Camera Control Bus)双线串行接口,MCU通过SCCB接口配置和读取OV2640的信息。MCU通过并行总线的方式来接收OV2640的图像数据。

Y(2..9)为8位MSB(MostSignificant Bit,最高有效位模式)并行总线,SDIO、SCLK为SCCB接口,PCLK为像素时钟输出管脚(每一个周期从并行总线上输出一个像素)。VSYNC为列同步输出管脚(每帧图像发生一次跳变),HERF为行參考输出管脚(每一个周期总线从并行总线上输出一行图像数据)。

 

系统上电后,MCU配置OV2640的工作方式,初始化LD3320,然后检查LD3320的状态。当LD3320的状态是“找到识别结果”,开启OV2640中断,在OV2640准备好图像后。VSYNC会被拉高一段时间,MCU通过PCLK上升沿中断按字节接收图像数据,接收数据完毕。关闭OV2640中断。

然后向新浪微博发送已经写进程序里的自己想说的话和接收到的图片。接下来将对基本的程序块做介绍。

 

程序介绍

在《为你的设备加入社交网络功能》中,已经具体介绍了OV2640的初始化配置程序,本篇文章就不再赘述。图像数据缓存程序与本文稍有不同,这里简介图像数据缓存程序。本文对LD3320的写入词条列表,启动语音识别,中断处理程序。发送微博程序做主要介绍。

图像数据缓存程序(摘至stm32f10x_it.c):

void EXTI0_IRQHandler(void)

{

u8 temp;

EXTI_ClearITPendingBit(EXTI_Line0);     //清除EXTI0线路挂起位

 if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_1)==0)return;//HREF管脚为低

 temp=(u8)((GPIOC->IDR)>>8 & 0x00ff);    //读取一个字节图像数据

switch(jpg_flag)

 {

case 0:

      if(temp==0xff)                      //图像数据以0xff 0xd8开头

      {  

JPEGBuffer[0]=0xff;

jpg_flag=1;

      }

break;

case 1:

if(temp==0xd8)

      {

JPEGBuffer[1]=0xd8;

jpg_flag=2;

JPEGCnt=2;

      }

else if(temp!=0xff)

jpg_flag=0;

break;

case 2:

JPEGBuffer[JPEGCnt++] =temp;  //存储数据

if(temp==0xff)jpg_flag=3;

break;

case 3:

JPEGBuffer[JPEGCnt++]=temp;  //图像数据以0xff0xd9结尾

if(temp==0xd9)

      {

jpg_flag=4;

      }

else if(temp!=0xff)

jpg_flag=2;

break;

case 4:

break;

 }

}

在中断函数中通过以上程序就可以正确读取每一帧图像的数据了。程序思想已经在拍摄照片流程图中体现。JPEGBuffer为一个全局的图像缓存区。在主函数中,检測到缓存区数据准备完成后。就能够将图像发送给新浪微博了。

LD3320加入词条程序(摘至LD3320_main.c)

uint8 LD_AsrAddFixed(void)

{     

uint8 k, flag;

uint8nAsrAddLength;

#define DATE_A 4    /*数组二维数值*/

#define DATE_B  20     /*数组一维数值*/

uint8 sRecog[DATE_A][DATE_B] = {

                                                                      "wenjuan",\

                                                                      "guocui",\

                                                                       "jierui",\

                                                                        "chenge"\

                                                               };    /*加入关键词*/

                                     uint8  pCode[DATE_A] = {

                                                                      CODE_wenjuan,\

                                                                      CODE_guocui,\

                                                                      CODE_jierui,\

                                                                      CODE_chenge\

                                                        };    /*加入识别码*/

flag = 1;

for (k=0; k<DATE_A; k++)

{                           

if(LD_Check_ASRBusyFlag_b2() == 0)

                               {

                                   flag= 0;

                                   break;

                                 }

LD_WriteReg(0xc1, pCode[k] );

LD_WriteReg(0xc3, 0 );

LD_WriteReg(0x08, 0x04);

LD3320_delay(1);

LD_WriteReg(0x08, 0x00);

LD3320_delay(1);

for (nAsrAddLength=0; nAsrAddLength<DATE_B;nAsrAddLength++)

{

if (sRecog[k][nAsrAddLength] == 0)

break;

LD_WriteReg(0x5, sRecog[k][nAsrAddLength]);

}

LD_WriteReg(0xb9, nAsrAddLength);

LD_WriteReg(0xb2, 0xff);

LD_WriteReg(0x37, 0x04);

LD_WriteReg(0x37, 0x04);

}    

return flag;

}

列表的规则是,每一个识别条目相应一个特定的编号(1个字节),不同的识别条目的编号能够同样,并且不用连续。

本芯片最多支持50个识别条目,每一个识别条目是标准普通话的汉语拼音(小写),每2个字(汉语拼音)之间用一个空格间隔。首先把识别条目的编号写入0xc1寄存器,其次,将字符串中的字符按顺序写入寄存器0x05。然后将字符串长度写入寄存器0xB9,向寄存器0xB2写入0xFF。向寄存器0x37写入0x04。通知DSP要加入一项识别句。

LD3320启动语音识别程序(摘至LD3320_main.c)

uint8 LD_AsrRun(void)

{

1     LD_WriteReg(0x35,MIC_VOL);   //ADC增益设置

2     LD_WriteReg(0x1C,0x09);      //ADC开关控制。写09H为保留命令字。

3     LD_WriteReg(0xBD,0x20);      //初始化控制寄存器,写入20H,保留命令字。

4   LD_WriteReg(0x08, 0x01);     //清除FIFO内容。第0位:写入1→清除FIFO_DATA。

       LD3320_delay( 5); 

5     LD_WriteReg(0x08,0x00);      

       LD3320_delay( 5);

6     if(LD_Check_ASRBusyFlag_b2()== 0)  //检查b2寄存器是否为空暇

       {

              return 0;

       }

       LD_WriteReg(0xB2,0xff);  

7   LD_WriteReg(0x37, 0x06); //语音识别控制命令下发寄存器,写06H:通知DSP開始识别语音。

       LD_WriteReg(0x37,0x06);

       LD3320_delay( 5);

8  LD_WriteReg(0x1C, 0x0b); // ADC开关控制写0BH麦克风输入ADC通道可用

LD_WriteReg(0x29, 0x10); // 中断同意(可读写)第4位:同步中断同意,1表示同意;0表示不同意。

LD_WriteReg(0xBD, 0x00);           // 初始化控制寄存器写入00H。然后启动;为ASR模块。

       return 1;

}

第1行,ADC增益设置。或能够理解为麦克风(MIC)音量。能够设置为00H-7FH。建议设置值为40H-55H:值越大代表MIC音量越大,识别启动越敏感,但可能带来很多其它误识别;值越小代表MIC音量越小,须要近距离说话才干启动识别功能,优点是对远处的干扰语音没有反应。第6行检查LD3320是否为空暇状态。假设为空暇状态。在第7行向0x37寄存器写入0x06。通知DSP開始语音识别。第8行,向寄存器0x1c写入0x0b。表示麦克风输入ADC通道可用。

LD3320中断处理程序(摘至LDChip.c)

voidProcessInt0(void)

{

uint8nAsrResCount=0;

1  ucRegVal = LD_ReadReg(0x2B);  // 读取中断请求编号寄存器

2   LD_WriteReg(0x29,0);  // 第2位:FIFO 中断同意,1表示同意;0表示不同意。

第4位:同步中断同意,1表示同意;0表示不同意。

3   LD_WriteReg(0x02,0);  // FIFO中断同意第0位:同意FIFO_DATA中断;第2位:同意FIFO_EXT中断。

4   If((ucRegVal& 0x10)&&LD_ReadReg(0xb2)==0x21 &&LD_ReadReg(0xbf)==0x35)        // 中断请求编号寄存器0x2B第4位:读取值为1表示语音识别有结果产生;MCU可清零。ASR过程中DSP忙闲状态寄存器0xb2,读取到0x21 表示闲,查询到为闲状态能够进行下一步ASR动作。ASR状态报告寄存器0xbf 读到数值为0x35,能够确定是一次语音识别流程正常结束.

             {   

5                  nAsrResCount= LD_ReadReg(0xba); //中断辅助信息寄存器,当中的数值表示语音识别有几个识别候选

6                  if(nAsrResCount>0&&nAsrResCount<=4)

               {

nAsrStatus=LD_ASR_FOUNDOK;                         

               }

else

               {

7nAsrStatus=LD_ASR_FOUNDZERO;

               }     

}

else

{

8  nAsrStatus=LD_ASR_FOUNDZERO;    //运行没有识别

}

LD_WriteReg(0x2b,0);

LD_WriteReg(0x1C,0);/*写0:ADC不可用*/

LD_WriteReg(0x29,0);

LD_WriteReg(0x02,0);

LD_WriteReg(0x2B,0);

LD_WriteReg(0xBA,0); 

LD_WriteReg(0xBC,0);  

LD_WriteReg(0x08,1);    /*清除FIFO_DATA*/

LD_WriteReg(0x08,0);    /*清除FIFO_DATA后再次写0*/

}

中断处理函数的第1行读取中断请求编号寄存器0x2B的值,第4位:读取值为1表示语音识别有结果产生;MCU可清零。第2位:读取值为1表示芯片内部FIFO中断发生。MP3播放时会产生中断标志请求外部MCU向FIFO_DATA中Reload数据。第3位:读取值为1表示芯片内部已经出现错误。值得注意的是:假设在中断响应时读到这位为1。须要对芯片进行重新启动Reset。才干够继续工作。第2,3行关闭LD3320的中断。第4行,读取中断请求编号寄存器0x2B的值,当第4位读取值为1表示语音识别有结果产生,其次读取语音识别过程中DSP忙闲状态寄存器0xb2,读取到0x21表示闲,然后读取语音识别状态报告寄存器0xbf的值,读到数值为0x35,能够确定是一次语音识别流程正常结束,当这三个寄存器的数值不满足以上要求的时候。返回“LD_ASR_FOUNDZERO”,表示未找到识别结果。当满足以上要求时,第5行,读取中断辅助信息寄存器,当中的数值表示语音识别有几个识别候选。当数值为 1 – 4: 表示有N个识别候选,数值为0或者大于4表示没有识别候选,当有识别候选的时候,返回“ LD_ASR_FOUNDOK。表示找到语音识别结果。

发送微博程序(摘至weibo.c)

unsigned char post_weibo_upload(char* weibo, uint8* pic,uint32 picLen)

{

unsigned char ret=0;

unsignedintlen=0;

1  if(socket(SOCK_WEIBO,Sn_MR_TCP,any_local_port++,0)!=1)  //to initialize a TCP socket

  {

printf("Socket initialization failed.\r\n");

return 0;

  }

else

  {

printf("Connect with Weibo server.\r\n");

2   ret=connect(SOCK_WEIBO,weibo_server_ip,80);       //connect to the weibo server, default TCPport is 80

if(ret!=1)

    {

printf("Connect Weibo server failed.\r\n");

return 0;

    }

else

    {

3while(getSn_SR(SOCK_WEIBO)!=SOCK_ESTABLISHED);  //wait for the TCP connection established!

printf("Connected with Weiboserver.\r\n");

4   sprintf(post_data,"\r\n--%s\r\nContent-Disposition:form-data; name=\"id\"\r\n\r\n%s"\

"\r\n--%s\r\nContent-Disposition:form-data; name=\"pw\"\r\n\r\n%s"\

"\r\n--%s\r\nContent-Disposition:form-data; name=\"cmd\"\r\n\r\nupload"\

"\r\n--%s\r\nContent-Disposition:form-data; name=\"status\"\r\n\r\n%s"\

"\r\n--%s\r\nContent-Disposition:form-data; name=\"file\"; filename=\"pic.jpg\"\r\nContent-Type:application/octet-stream\r\n\r\n",(char*)BOUNDARY,(char*)WEIBO_ID,(char*)BOUNDARY,(char*)WEIBO_PWD,(char*)BOUNDARY,(char*)BOUNDARY,weibo,(char*)BOUNDARY);//"\r\n--%s--\r\n"

5   sprintf(tmp_buf,"POST %sHTTP/1.1\r\nHost: %s\r\nUser-Agent: w5500\r\nContent-Type: multipart/form-data;boundary=%s\r\nConnection:close\r\nContent-Length:%d\r\n\r\n%s",(char*)HTTP_PATH,(char*)WEIBO_SERVER,(char*)BOUNDARY,strlen(post_data)+picLen+strlen((char*)BOUNDARY)+8,post_data);

6   len=send(SOCK_WEIBO,(unsignedchar*)tmp_buf,strlen(tmp_buf)); //upload your weibo content

      uint16 file_len=picLen;

     uint16 send_len=0;

while(file_len)

      {

if(file_len>PACKET_LEN)

        {

if(getSn_SR(SOCK_WEIBO)!=SOCK_ESTABLISHED)

          {

return 0;

          }

7send(SOCK_WEIBO, (uint8*)(pic+send_len), PACKET_LEN);  // upload picture

send_len+=PACKET_LEN;

file_len-=PACKET_LEN;

        }

else

        {

8send(SOCK_WEIBO, (uint8*)(pic+send_len), file_len);// uploadpicture

send_len+=file_len;

file_len-=file_len;

        }

      }

sprintf(tmp_buf,"\r\n--%s--\r\n",(char*)BOUNDARY);

send(SOCK_WEIBO,(unsigned char*)tmp_buf,strlen(tmp_buf));

while(1)

      {

9len=getSn_RX_RSR(SOCK_WEIBO);

if(len>0)

        {

memset(tmp_buf,0x00,MAX_BUF_SIZE);

10len=recv(SOCK_WEIBO,(unsigned char*)tmp_buf, len);  //receive thereturn result from weibo server

11char*p=strstr(tmp_buf,(char*)"\r\n\r\n")+4;   //gethttp payload without http headerprintf("%s\r\n",p);

disconnect(SOCK_WEIBO);  //disconnect with weibo server

close(SOCK_WEIBO);       //close the socket

return 1;                //sucess! return 1

        }

      }

    }

  }

}

发送微博函数的第1行,初始化一个socket。第2行,对server发出连接请求,第3行一直等待连接的建立。

与server建立连接后。第4。5行负责组建带有微博内容和图片长度的HTTP数据包,第6行负责发送微博内容,第7,8行发送图片数据。第9行是读取W5500接收到的数据长度,第10行从W5500的接收缓存中把接收到的数据读到tmp_buf中。

因为接收到的数据包括了HTTP头,第11行是把HTTP头去掉,得到server的返回结果。server返回结果的类型请參看《为你的设备加入社交网络功能》一文。

好了。代码就这么多,赶快编译烧到单片机里面吧,上电,对着麦克风说出一句已经写到LD3320里的话,当相应的指示灯亮或者闪烁。说明已经识别成功,然后对着摄像头微笑吧,这时摄像头为我们拍张照片,上传微博,然后看串口调试信息。假设收到“255:ok”,那就成功了,登录到微博看看,写进程序里的话以及自己的照片出如今微博上面。如图11。

 

图11系统发送微博效果图

至此。我们的基于语音识别的微博签到系统已经大功告成,你心动了吗?赶快制作你自己的微博签到系统吧。