战舰V3的录音程序解析

上一章,我们实现了一个简单的音乐播放器,本章我们将在上一章的基础上,实现一个简单的录音机,实现WAV录音。本章分为如下几个部:

50.1 WAV简介

50.2 硬件设计

50.3 软件设计

50.4 下载验证

50.1 WAV简介

WAV即WAVE文件,WAV是计算机领域最常用的数字化声音文件格式之一,它是微软专门为Windows系统定义的波形文件格式(Waveform Audio),由于其扩展名为"*.wav"。它符合RIFF(Resource Interchange File Format)文件规范,用于保存Windows平台的音频信息资源,被Windows平台及其应用程序所广泛支持,该格式也支持MSADPCM,CCITT A LAW等多种压缩运算法,支持多种音频数字,取样频率和声道,标准格式化的WAV文件和CD格式一样,也是44.1K的取样频率,16位量化数字,因此在声音文件质量和CD相差无几!

ALIENTEK战舰STM32开发板板载的VS1053支持2种格式的WAV录音:PCM格式或者IMA ADPCM格式,其中PCM(脉冲编码调制)是最基本的WAVE文件格式,这种文件直接存储采样的声音数据没有经过任何的压缩。而IAM ADPCM则是使用了压缩算法,压缩比率为4:1。

本章,我们主要讨论PCM,因为这个最简单。我们将利用VS1053实现16位,8Khz采样率的单声道WAV录音(PCM格式)。要想实现WAV录音得先了解一下WAV文件的格式,WAVE文件是由若干个Chunk组成的。按照在文件中的出现位置包括:RIFF WAVE Chunk、 Format Chunk、 Fact Chunk(可选)和 Data Chunk。每个Chunk由块标识符、数据大小和数据三部分组成,如图50.1.1所示:

其中块标识符由4个ASCII码构成,数据大小则标出紧跟其后的数据的长度(单位为字节),注意这个长度不包含块标识符和数据大小的长度,即不包含最前面的8个字节。所以实际Chunk的大小为数据大小加8。

首先,我们来看看RIFF块(RIFF WAVE Chunk),该块以“RIFF”作为标示,紧跟wav文件大小(该大小是wav文件的总大小-8),然后数据段为“WAVE”,表示是wav文件。RIFF块的Chunk结构如下:

//RIFF块

typedef __packed struct

{

u32 ChunkID;             //chunk id;这里固定为"RIFF",即0X46464952

u32 ChunkSize ;            //集合大小;文件总大小-8

u32 Format;                //格式;WAVE,即0X45564157

}ChunkRIFF ;

接着,我们看看Format块(Format Chunk),该块以“fmt ”作为标示(注意有个空格!),一般情况下,该段的大小为16个字节,但是有些软件生成的wav格式,该部分可能有18个字节,含有2个字节的附加信息。Format块的Chunk结构如下:

//fmt块

typedef __packed struct

{

u32 ChunkID;             //chunk id;这里固定为"fmt ",即0X20746D66

u32 ChunkSize ;            //子集合大小(不包括ID和Size);这里为:20.

u16 AudioFormat;        //音频格式;0X10,表示线性PCM;0X11表示IMA ADPCM

u16 NumOfChannels;    //通道数量;1,表示单声道;2,表示双声道;

u32 SampleRate;           //采样率;0X1F40,表示8Khz

u32 ByteRate;               //字节速率;

u16 BlockAlign;            //块对齐(字节);

u16 BitsPerSample;              //单个采样数据大小;4位ADPCM,设置为4

}ChunkFMT;

接下来,我们再看看Fact块(Fact Chunk),该块为可选块,以“fact”作为标示,不是每个WAV文件都有,在非PCM格式的文件中,一般会在Format结构后面加入一个Fact块,该块Chunk结构如下:

//fact块

typedef __packed struct

{

u32 ChunkID;                    //chunk id;这里固定为"fact",即0X74636166;

u32 ChunkSize ;                 //子集合大小(不包括ID和Size);这里为:4.

u32 DataFactSize;               //数据转换为PCM格式后的大小

}ChunkFACT;

DataFactSize是这个Chunk中最重要的数据,如果这是某种压缩格式的声音文件,那么从这里就可以知道他解压缩后的大小。对于解压时的计算会有很大的好处!不过本章我们使用的是PCM格式,所以不存在这个块。

最后,我们来看看数据块(Data Chunk),该块是真正保存wav数据的地方,以“data”'作为该Chunk的标示。然后是数据的大小。紧接着就是wav数据。根据Format Chunk中的声道数以及采样bit数,wav数据的bit位置可以分成如表50.1.1所示的几种形式:

本章,我们采用的是16位,单声道,所以每个取样为2个字节,低字节在前,高字节在后。数据块的Chunk结构如下:

//data块

typedef __packed struct

{

u32 ChunkID;             //chunk id;这里固定为"data",即0X61746164

u32 ChunkSize ;            //子集合大小(不包括ID和Size);文件大小-60.

}ChunkDATA;

通过以上学习,我们对WAVE文件有了个大概了解。接下来,我们看看如何使用VS1053实现WAV(PCM格式)录音。

激活PCM录音

VS1053激活PCM录音需要设置的寄存器和相关位如表50.1.2所示:

通过设置SCI_MODE寄存器的2、12、14位,来激活PCM录音,SCI_MODE的各位描述见表49.1.4(也可以参考VS1053的数据手册)。SCI_AICTRL0寄存器用于设置采样率,我们本章用的是8K的采样率,所以设置这个值为8000即可。SCI_AICTRL1寄存器用于设置AGC,1024相当于数字增加1,这里建议大家设置AGC在4(4*1024)左右比较合适。SCI_AICTRL2用于设置自动AGC的时候的最大值,当设置为0的时候表示最大64(65536),这个大家按自己的需要设置即可。最后,SCI_AICTRL3,我们本章用到的是咪头线性PCM单声道录音,所以设置该寄存器值为6。

通过这几个寄存器的设置,我们就激活VS1053的PCM录音了。不过,VS1053的PCM录音有一个小BUG,必须通过加载patch才能解决,如果不加载patch,那么VS1053是不输出PCM数据的,VLSI提供了我们这个patch,只需要通过软件加载即可。

读取PCM数据

在激活了PCM录音之后,SCI_HDAT0和SCI_HDAT1有了新的功能。VS1053的PCM采样缓冲区由1024个16位数据组成,如果SCI_HDAT1大于0,则说明可以从SCI_HDAT0读取至少SCI_HDAT1个16位数据,如果数据没有被及时读取,那么将溢出,并返回空的状态。

注意,如果SCI_HDAT1≥896,最好等待缓冲区溢出,以免数据混叠。所以,对我们来说,只需要判断SCI_HDAT1的值非零,然后从SCI_HDAT0读取对应长度的数据,即完成一次数据读取,以此循环,即可实现PCM数据的持续采集。

最后,我们看看本章实现WAV录音需要经过哪些步骤:

1)设置VS1053 PCM采样参数

这一步,我们要设置PCM的格式(线性PCM)、采样率(8K)、位数(16位)、通道数(单声道)等重要参数,同时还要选择采样通道(咪头),还包括AGC设置等。可以说这里的设置直接决定了我们wav文件的性质。

2)激活VS1053的PCM模式,加载patch

通过激活VS1053的PCM格式,让其开始PCM数据采集,同时,由于VS1053的BUG,我们需要加载patch,以实现正常的PCM数据接收。

3)创建WAV文件,并保存wav

在前两部设置成功之后,我们即可正常的从SCI_HDAT0读取我们需要的PCM数据了,不过在这之前,我们需要先在创建一个新的文件,并写入wav头,然后才能开始写入我们的PCM数据。

4)读取PCM数据

经过前面几步的处理,这一步就比较简单了,只需要不停的从SCI_HDAT0读取数据,然后存入wav文件即可,不过这里我们还需要做文件大小统计,在最后的时候写入wav头里面。

5)计算整个文件大小,重新保存wav头并关闭文件

在结束录音的时候,我们必须知道本次录音的大小(数据大小和整个文件大小),然后更新wav头,重新写入文件,最后因为FATFS,在文件创建之后,必须调用f_close,文件才会真正体现在文件系统里面,否则是不会写入的!所以最后还需要调用f_close,以保存文件。

50.2 硬件设计

本章实验功能简介:开机的时候先检测字库,然后初始化VS1053,进行RAM测试和正弦测试,之后,检测SD卡根目录是否存在RECORDER文件夹,如果不存在则创建,如果创建失败,则报错。在找到SD卡的RECORDER文件夹后,即设置VS1053进入录音模式,此时可以在耳机听到VS1053采集的音频。KEY0用于开始/暂停录音,KEY2用于保存并停止录音,WK_UP用于AGC增加、KEY1用于AGC减小,TPAD用于播放最近一次的录音。当我们按下KEY0的时候,可以在屏幕上看到录音文件的名字,以及录音时间,然后通过KEY2可以保存该文件,同时停止录音(文件名和时间也都将清零),在完成一个录音后,我们可以通过按TPAD按键,来试听刚刚的录音。DS0用于提示程序正在运行,DS1用于指示当前是否处于录音暂停状态。

本实验用到的资源如下:

1)  指示灯DS0和DS1

2)  五个按键(WK_UP/KEY0/KEY1/KEY2/TPAD)

3)  串口

4)  TFTLCD模块

5)  SD卡

6)  SPI FLASH

7)  VS1053

8)  74HC4052

9)  TDA1308

本章用到的硬件资源同上一章基本一样,就多了一个TPAD按键,用于播放最近一次录音。

本实验,大家需要准备1个SD卡和一个耳机,分别插入SD卡接口和耳机接口,然后下载本实验就可以实现一个简单的录音机了。

50.3 软件设计

打开上一章的工程,首先在APP文件夹下面新建recorder.c和recorder.h两个文件,然后将recorder.c加入到工程的APP组下。

因为recorder.c代码比较多,我们这里仅介绍其中的三个函数,首先是设置VS1053进入PCM模式的函数:recoder_enter_rec_mode,该函数代码如下:

//进入PCM 录音模式

//agc:0,自动增益.1024相当于1倍,512相当于0.5倍,最大值65535=64倍

void recoder_enter_rec_mode(u16 agc)

{

//如果是IMA ADPCM,采样率计算公式如下:

//采样率=CLKI/256*d;

//假设d=0,并2倍频,外部晶振为12.288M.那么Fc=(2*12288000)/256*6=16Khz

//如果是线性PCM,采样率直接就写采样值

VS_WR_Cmd(SPI_BASS,0x0000);

VS_WR_Cmd(SPI_AICTRL0,8000);   //设置采样率,设置为8Khz

VS_WR_Cmd(SPI_AICTRL1,agc);

//设置增益,0,自动增益.1024相当于1倍,512相当于0.5倍,最大值65535=64倍

VS_WR_Cmd(SPI_AICTRL2,0);         //设置增益最大值,0,代表最大值65536=64X

VS_WR_Cmd(SPI_AICTRL3,6);         //左通道(MIC单声道输入)

VS_WR_Cmd(SPI_CLOCKF,0X2000);

//设置VS10XX的时钟,MULT:2倍频;ADD:不允许;CLK:12.288Mhz

VS_WR_Cmd(SPI_MODE,0x1804);    //MIC,录音激活

delay_ms(5);                                      //等待至少1.35ms

VS_Load_Patch((u16*)wav_plugin,40);//VS1053的WAV录音需要patch

}

该函数就是用我们前面介绍的方法,激活VS1053的PCM模式,本章,我们使用的是8Khz采样率,16位单声道线性PCM模式,AGC通过函数参数设置。最后加载patch(用于修复VS1053录音BUG)。

第二个函数是初始化wav头的函数:recoder_wav_init,该函数代码如下:

//初始化WAV头.

void recoder_wav_init(__WaveHeader* wavhead) //初始化WAV头

{

wavhead->riff.ChunkID=0X46464952;        //"RIFF"

wavhead->riff.ChunkSize=0;                      //还未确定,最后需要计算

wavhead->riff.Format=0X45564157;          //"WAVE"

wavhead->fmt.ChunkID=0X20746D66;      //"fmt "

wavhead->fmt.ChunkSize=16;                   //大小为16个字节

wavhead->fmt.AudioFormat=0X01;           //0X01,表示PCM;0X01,表示IMA ADPCM

wavhead->fmt.NumOfChannels=1;             //单声道

wavhead->fmt.SampleRate=8000;               //8Khz采样率 采样速率

wavhead->fmt.ByteRate=wavhead->fmt.SampleRate*2;//16位,即2个字节

wavhead->fmt.BlockAlign=2;                     //块大小,2个字节为一个块

wavhead->fmt.BitsPerSample=16;                     //16位PCM

wavhead->data.ChunkID=0X61746164;       //"data"

wavhead->data.ChunkSize=0;                     //数据大小,还需要计算

}

该函数初始化wav头的绝大部分数据,这里我们设置了该wav文件为8Khz采样率,16位线性PCM格式,另外由于录音还未真正开始,所以文件大小和数据大小都还是未知的,要等录音结束才能知道。该函数__WaveHeader结构体就是由前面介绍的三个Chunk组成,结构为:

//wav头

typedef __packed struct

{

ChunkRIFF riff;     //riff块

ChunkFMT fmt;  //fmt块

//ChunkFACT fact; //fact块 线性PCM,没有这个结构体

ChunkDATA data;   //data块

}__WaveHeader;

最后,我们介绍recoder_play函数,是录音机实现的主循环函数,该函数代码如下:

//录音机

//所有录音文件,均保存在SD卡RECORDER文件夹内.

u8 recoder_play(void)

{

u8 res, key, rval=0;

__WaveHeader *wavhead=0;

u32 sectorsize=0;u16 w; u16 idx=0;

FIL* f_rec=0;                             //文件

DIR recdir;                               //目录

u8 *recbuf;                                 //数据内存

u8 rec_sta=0;                              //录音状态

//[7]:0,没有录音;1,有录音;

//[6:1]:保留

//[0]:0,正在录音;1,暂停录音;

u8 *pname=0;

u8 timecnt=0;                             //计时器

u32 recsec=0;                              //录音时间

u8 recagc=4;                               //默认增益为4

while(f_opendir(&recdir,"0:/RECORDER"))//打开录音文件夹

{

Show_Str(60,230,240,16,"RECORDER文件夹错误!",16,0); delay_ms(200);

LCD_Fill(60,230,240,246,WHITE); delay_ms(200);          //清除显示

f_mkdir("0:/RECORDER");//创建该目录

}

f_rec=(FIL *)mymalloc(SRAMIN,sizeof(FIL));   //开辟FIL字节的内存区域

if(f_rec==NULL)rval=1;       //申请失败

wavhead=(__WaveHeader*)mymalloc(SRAMIN,sizeof(__WaveHeader));

//开辟__WaveHeader字节的内存区域

if(wavhead==NULL)rval=1;

recbuf=mymalloc(SRAMIN,512);

if(recbuf==NULL)rval=1;

pname=mymalloc(SRAMIN,30);

//申请30个字节内存,存放路径+名字,类似"0:RECORDER/REC00001.wav"

if(pname==NULL)rval=1;

if(rval==0)                                                      //内存申请OK

{

recoder_enter_rec_mode(1024*recagc);

while(VS_RD_Reg(SPI_HDAT1)>>8);        //等到buf 较为空闲再开始

recoder_show_time(recsec);                        //显示时间

recoder_show_agc(recagc);                         //显示agc

pname[0]=0;                                             //pname没有任何文件名

while(rval==0)

{

key=KEY_Scan(0);

switch(key)

{

case KEY_LEFT:   //STOP&SAVE

if(rec_sta&0X80)//有录音

{

wavhead->riff.ChunkSize=sectorsize*512+36;     //文件大小-8;

wavhead->data.ChunkSize=sectorsize*512;         //数据大小

f_lseek(f_rec,0);                                               //偏移到文件头.

f_write(f_rec,(const void*)wavhead,sizeof(__WaveHeader),

&bw);//写入头数据

f_close(f_rec); sectorsize=0;

}

rec_sta=0; recsec=0; LED1=1; //关闭DS1

LCD_Fill(60,230,240,246,WHITE);

//清除显示,清除之前显示的录音文件名

recoder_show_time(recsec);          //显示时间

break;

case KEY_RIGHT: //REC/PAUSE

if(rec_sta&0X01)//原来是暂停,继续录音

{

rec_sta&=0XFE;//取消暂停

}else if(rec_sta&0X80)//已经在录音了,暂停

{

rec_sta|=0X01;       //暂停

}else                            //还没开始录音

{

rec_sta|=0X80;       //开始录音

recoder_new_pathname(pname);                 //得到新的名字

Show_Str(60,230,240,16,pname+11,16,0);  //显示录音文件名字

recoder_wav_init(wavhead);                       //初始化wav数据

res=f_open(f_rec,(const TCHAR*)pname,FA_CREATE_ALWAYS

|FA_WRITE);

if(res)//文件创建失败

{

rec_sta=0;       //创建文件失败,不能录音

rval=0XFE;    //提示是否存在SD卡

}else res=f_write(f_rec,(const void*)wavhead,

sizeof(__WaveHeader),&bw);//写入头数据

}

if(rec_sta&0X01)LED1=0;    //提示正在暂停

else LED1=1;

break;

case KEY_UP:       //AGC+

case KEY_DOWN: //AGC-

if(key==KEY_UP)recagc++;

else if(recagc)recagc--;

if(recagc>15)recagc=15;       //范围限定为0~15,自动AGC.其他AGC倍数

recoder_show_agc(recagc);

VS_WR_Cmd(SPI_AICTRL1,1024*recagc);

//设置增益,0,自动增益.1024相当于1倍,512相当于0.5倍

break;

}

if(rec_sta==0X80)//已经在录音了

{

w=VS_RD_Reg(SPI_HDAT1);

if((w>=256)&&(w<896))

{

idx=0;

while(idx<512)     //一次读取512字节

{

w=VS_RD_Reg(SPI_HDAT0);

recbuf[idx++]=w&0XFF; recbuf[idx++]=w>>8;

}

res=f_write(f_rec,recbuf,512,&bw);//写入文件

if(res) break;//写入出错.

sectorsize++;//扇区数增加1,约为32ms

}

}else//没有开始录音,则检测TPAD按键

{

if(TPAD_Scan(0)&&pname[0])//如果触摸按键被按下,且pname不为空

{

Show_Str(60,230,240,16,"播放:",16,0);

Show_Str(60+40,230,240,16,pname+11,16,0);//显示播放的文件名字

rec_play_wav(pname);                        播放pname

LCD_Fill(60,230,240,246,WHITE);     /清除之前显示的录音文件名

recoder_enter_rec_mode(1024*recagc);       //重新进入录音模式

while(VS_RD_Reg(SPI_HDAT1)>>8); //等到buf 较为空闲再开始

recoder_show_time(recsec);                 //显示时间

recoder_show_agc(recagc);                  //显示agc

}

delay_ms(5); timecnt++;

if((timecnt%20)==0)LED0=!LED0;//DS0闪烁

}

if(recsec!=(sectorsize*4/125))//录音时间显示

{

LED0=!LED0;//DS0闪烁

recsec=sectorsize*4/125;

recoder_show_time(recsec);//显示时间

}

}

}

myfree(SRAMIN,wavhead); myfree(SRAMIN,recbuf);

myfree(SRAMIN,f_rec);       myfree(SRAMIN,pname);

return rval;

}

该函数实现了我们在硬件设计时介绍的功能,我们就不详细介绍了。recorder.c的其他代码和recorder.h的代码我们这里就不再贴出了,请大家参考光盘本实验的源码。保存recorder.c,最后,我们在test.c里面修改main函数如下:

int main(void)

{

Stm32_Clock_Init(9);    //系统时钟设置

delay_init(72);                     //延时初始化

uart_init(72,9600);       //串口1初始化

LCD_Init();                  //初始化液晶

LED_Init();           //LED初始化

KEY_Init();                  //按键初始化

TPAD_Init(72);             //初始化触摸按键

Audiosel_Init();            //初始化音源选择

usmart_dev.init(72);      //usmart初始化

mem_init(SRAMIN);     //初始化内部内存池

VS_Init();

exfuns_init();                //为fatfs相关变量申请内存

f_mount(0,fs[0]);         //挂载SD卡

f_mount(1,fs[1]);         //挂载FLASH.

POINT_COLOR=RED;

while(font_init())         //检查字库

{

LCD_ShowString(60,50,200,16,16,"Font Error!"); delay_ms(200);

LCD_Fill(60,50,240,66,WHITE);//清除显示

}

Show_Str(60,50,200,16,"战舰 STM32开发板",16,0);

Show_Str(60,70,200,16,"WAV录音机实验",16,0);

Show_Str(60,90,200,16,"广州星翼电子",16,0);

Show_Str(60,110,200,16,"2012年9月20日",16,0);

Show_Str(60,130,200,16,"KEY0:REC/PAUSE",16,0);

Show_Str(60,150,200,16,"KEY2:STOP&SAVE",16,0);

Show_Str(60,170,200,16,"KEY_UP:AGC+ KEY1:AGC-",16,0);

Show_Str(60,190,200,16,"TPADlay The File",16,0);

while(1)

{

Audiosel_Set(0);   //MP3通道

Show_Str(60,210,200,16,"存储器测试...",16,0);

VS_Ram_Test();

Show_Str(60,210,200,16,"正弦波测试...",16,0);

VS_Sine_Test();

Show_Str(60,210,200,16,"<<WAV录音机>>",16,0);

recoder_play();

}

}

该函数代码同上一章的main函数代码几乎一样,只是我们这里增加了TPAD初始化,然后修改了一些显示内容,其他两者就都差不多了,我们就不再细说了。

至此,本实验的软件设计部分结束。

50.4 下载验证

在代码编译成功之后,我们下载代码到ALIENTEK战舰STM32开发板上,程序先检测字库,然后对VS1053进行RAM测试和正弦测试,之后检测SD卡的RECORDER文件夹,一切顺利通过之后,激活VS1053的PCM录音模式,得到,如图50.4.1所示:


图50.4.1 录音机界面

此时,我们按下KEY0就开始录音了,此时看到屏幕显示录音文件的名字以及录音时长,如图50.4.2所示:

图50.4.2 录音进行中

在录音的时候按下KEY0则执行暂停/继续录音的切换,通过DS1指示录音暂停,按WK_UP和KEY1可以调节AGC,AGC越大,越灵敏,不过不建议设置太大,因为这可能导致失真。通过按下KEY2,可以停止当前录音,并保存录音文件。在完成一次录音文件保存之后,我们可以通过按TPAD按键,来实现播放这个录音文件(即播放最近一次的录音文件),实现试听。

我们将开发板的录音文件放到电脑上面,可以通过属性查看录音文件的属性,如图50.4.3所示:

图50.4.3 录音文件属性

这和我们预期的效果一样,通过电脑端的播放器(winamp/千千静听等)可以直接播放我们所录的音频。经实测,效果还是非常不错的。

硬件——STM32 , 录音的更多相关文章

  1. 硬件——STM32 , 录音,wav

    详细的wav头文件解析,有例子:http://www.cnblogs.com/chulin/p/8918957.html 关于录音程序的编写: 我的思路是改写原子的程序,原子的程序需要借助VS1053 ...

  2. 硬件——STM32,ADC篇

    未完,待续...... 也就是stm32f10X系列的adc采集出来的结果是12位的 stm32f10X系列有两个16位adc 关于程序的编写方法:一般  “某某.c文件”:都是用来设置“某某”的一些 ...

  3. 硬件——STM32 , 软件框架

    单片机应用程序的框架大概有三种: 1,简单的前后台顺序执行程序. 2,时间片轮询法. 3,应用操作系统. 下面我们主要来讲解时间片轮询法: 在这里我们先介绍一下定时器的复用功能.就是使用1个定时器,可 ...

  4. 硬件——STM32 , SN74HC573锁存器

    74HC573是一款高速CMOS器件: 上图中:输出使能为:OE   锁存使能为:LE 典型电路: 上图中:PWR-AL-0,PWR-AL-1,PWR-AL-2:是单片机输出的高低电平给573 对应的 ...

  5. 关于STM32单片机的IAP实现

    基于STM32F103单片机的IAP实现(虽然该篇文章不会详细写出实现细节,但是会从一个全局的角度讲述,实际的实现细节只需根据datasheet即可完成). 一.基础概念 什么是IAP?IAP即在应用 ...

  6. Stm32复习之时钟系统

    地点:南图 这部分的内容是整个STM32学习知识的核心,不管是什么微控制器处理器,时钟系统都是其核心类似于人之心脏,因此学好理解这一章节至关重要. 为了便于理解这一系统,将从以下几个层次来讲.(忘了是 ...

  7. STM32下FatFs的移植,实现了坏块管理,硬件ECC,ECC纠错,并进行擦写均衡分析

    最近因项目需要,做一个数据采集的单片机平台.需要移植 FatFs .现在把最后成果贴上来. 1.摘要 在 STM32 单片机上,成功移植 FatFs 0.12b,使用的 Nand Flash 芯片为 ...

  8. STM32 硬件I2C 到底是不是个坑?

    /** ****************************************************************************** * @author    Maox ...

  9. MDK+硬件仿真器实现debugprintf()-stm32

    MDK+硬件仿真器实现debugprintf()-stm32 1MDK工程设置如下 2其中stm32debug.ini文件内容为 /********************************** ...

随机推荐

  1. 一个Java基础练习

    今天在群里又有一个朋友问到了这样一个练习,我索性将代码贴到这里,下次须要的朋友能够来这里看. 用到知识点:数组.集合.IO流 问题描写叙述:在例如以下图所看到的的一个txt文件里读取数据到内存,然后统 ...

  2. [UnityUI]循环滑动列表

    效果图: 使用的是UGUI和DOTween 当中比較关键的是循环滑动和层次排序: 1.循环滑动:这里先如果显示五张图片.分别标记为0,1,2,3,4,那么当向左滑动时,序列就变为1,2,3,4,0,这 ...

  3. Android 学习笔记之Bitmap位图的旋转

    位图的旋转也可以借助Matrix或者Canvas来实现. 通过postRotate方法设置旋转角度,然后用createBitmap方法创建一个经过旋转处理的Bitmap对象,最后用drawBitmap ...

  4. 慢慢人生路,学点Jakarta基础-深入剖析Java的接口和抽象类

    在java面向对象编程的,抽象类和接口始终存在有疑问的地方,因为两者太多相似有太多不同,在刚开始学习的时候经常弄的不对,使用情景搞混,今天来总结之前学习Java中接口和抽象类的问题. 抽象类 了解:只 ...

  5. vue 打包成 apk 文件(修改路径)

    第一个坑:文件引用路径 现在项目我们什么都没动,是初始化之后直接打包的状态,打开dist/index.htmnl文件整个网页都是一片空白的. 爬坑: 打开 config文件夹/index.js文件 a ...

  6. 网络最大流算法—Dinic算法及优化

    前置知识 网络最大流入门 前言 Dinic在信息学奥赛中是一种最常用的求网络最大流的算法. 它凭借着思路直观,代码难度小,性能优越等优势,深受广大oier青睐 思想 $Dinic$算法属于增广路算法. ...

  7. 通过CURL抓取页面中的图片路径并下载到本地

    1.首页是图片处理页面downpic.php <?phpfunction getImage($url,$filename="") { if($url=="" ...

  8. 项目EasyUi和JS中遇到的问题总汇

    近期因为项目用到EasyUi,曾经仅仅是听过,可是没有详细用过.仅仅能一边学一边做.如今将做的过程中遇到的一些难点总结例如以下,以备后用: EsayUi使用: Json格式:key:value,key ...

  9. 推断字符串string是数字、json结构、xml结构

    import org.json.JSONException; import org.json.JSONObject; import org.dom4j.DocumentException; impor ...

  10. 通过一个案例彻底读懂10046 trace--字节级深入破解

    转载请注明出处:http://blog.csdn.net/guoyjoe/article/details/37840583 2014.7.23晚20:30 Oracle support组猫大师分享&l ...