STM32照相机实验

BMP编码简介:

典型的BMP图像文件由4部分组成:

1,位图头文件数据结构,它包含BMP图像文件的类型、显示内容等信息;

2,位图信息数据结构,它包含有BMP图像的宽、高、压缩方法,以及定义颜色等信息;

3,调色板,这个部分是可选的,有些位图需要调色板,有些位图,比如真彩色图(24位的BMP)就不需要调色板;

4,位图数据,这部分的内容根据BMP位图使用的位数不同而不同,在24位图中直接使用RGB,而其他的小于24位的使用调色板中颜色索引值,16位也没有调色板由3个32位的数据替代,每个数据表示一个RGB掩码;

①BMP文件头(14字节):BMP文件头数据结构含有BMP文件的类型、文件大小和位图起始位置等信息。BMP文件头结构体定义如下:

//BMP文件头
typedef __packed struct
{
    u16  bfType ;     	//文件标志.只对'BM',用来识别BMP位图类型
    u32  bfSize ;	    //文件大小,占四个字节
    u16  bfReserved1 ;	//保留
    u16  bfReserved2 ;	//保留
    u32  bfOffBits ;  	//从文件开始到位图数据(bitmap data)开始之间的偏移量
}BITMAPFILEHEADER ;

②位图信息头(40字节):BMP位图信息头数据用于说明位图的尺寸等信息。 BMP位图信息头结构体定义如下:

typedef __packed struct
{
    u32 biSize ;	  	        //说明BITMAPINFOHEADER结构(本结构体)所需要的字数。
    long  biWidth ;		        //说明图象的宽度,以象素为单位 
    long  biHeight ;	   	    //说明图象的高度,以象素为单位 
    u16  biPlanes ;	   	        //为目标设备说明位面数,其值将总是被设为1 
    u16  biBitCount ;	   	    //说明比特数/象素,其值为1、4、8、16、24、或32
    u32 biCompression ;  	    //说明图象数据压缩的类型。其值可以是下述值之一:
			                    //0:BI_RGB:没有压缩;
			                    //1:BI_RLE8:每个象素8比特的RLE压缩编码,压缩格式由2字节组成 
   			                    //2:BI_RLE4:每个象素4比特的RLE压缩编码,压缩格式由2字节组成
  			                    //3:BI_BITFIELDS:每个象素的比特由指定的掩码决定。
    u32 biSizeImage ;		    //说明图象的大小,以字节为单位。当用BI_RGB格式时,可设置为0  
    long  biXPelsPerMeter ;		//说明水平分辨率,用象素/米表示
    long  biYPelsPerMeter ;		//说明垂直分辨率,用象素/米表示
    u32 biClrUsed ;	  	        //说明位图实际使用的彩色表中的颜色索引数
    u32 biClrImportant ;   		//说明对图象显示有重要影响的颜色索引的数目,
			                    //如果是0,表示都重要。 
}BITMAPINFOHEADER ;

设置biCompression的值时一般不会设置为BI_RLE84和BI_RLE8,经常设置为BI_BITFIELDS如果是16位图时会直接设置为BI_BITFIELDS。biSizeImage是根据biWidth、 biHeight、biBitCount计算出一个字节数来设置。剩下的几个一般设置为0。

③颜色表(调色板):颜色表用于说明位图中的颜色,它有若干个表项,每一个表项是一个RGBQUAD类型的结构,定义一种颜色,如下所示:

typedef __packed struct 
{
    u8 rgbBlue ;    	//指定蓝色强度
    u8 rgbGreen ;	    //指定绿色强度 
    u8 rgbRed ;	        //指定红色强度 
    u8 rgbReserved ;	//保留,设置为0 
}RGBQUAD ;

RGBQUAD结构数据的个数由biBitCount来确定:当biBitCount=1、4、8时,分别有2、16、256个表项;当biBitCount大于8时,没有颜色表项。          

BMP文件头、位图信息头和颜色表组成位图信息(我们将BMP文件头也加进来,方便处理),BITMAPINFO结构定义如下:

typedef __packed struct
{ 
    BITMAPFILEHEADER bmfHeader;
    BITMAPINFOHEADER bmiHeader;   
    RGBQUAD bmiColors[1];      //可以是2、16、256也可以没有,不过一般不使用这个会用另一个替代
}BITMAPINFO;   

④位图数据:记录了位图的每一个像素值,记录顺序是在扫描行内是从左到右,扫描行之间是从下到上。位图一个像素值所占字节数:

当biBitCount=1时,8个像素占1个字节;  

当biBitCount=4时,2个像素占1个字节;

当biBitCount=8时,1个像素占1个字节;

当biBitCount=16时,1个像素占2个字节;

当biBitCount=24时,1个像素占3个字节;

当biBitCount=32时,1个像素占4个字节; 

biBitCount=16,即高彩色(65K色)。当biCompression=BI_RGB(0),则采用RGB555格式,最高位恒为0;当biCompression= BI_BITFIELDS(3),则在原来调色板位置用3个DWORD类型的掩码替换,分别代表红、绿、蓝三色的掩码,一般是: 0X7C00(高5位)、0X03E0(中6位)、0X001F(低5位)。


我们采用16位BMP编码(因为LCD就是16位色的,而且16位BMP编码比24位BMP编码更省空间),故我们需要设置biBitCount的值为16,这样得到新的位图信息(BITMAPINFO)结构体

typedef __packed struct
{ 
    BITMAPFILEHEADER bmfHeader;
    BITMAPINFOHEADER bmiHeader;   
    u32 RGB_MASK[3];		//调色板用于存放RGB掩码
}BITMAPINFO;   

 RGB_MASK[3],即颜色掩码,分别代表红、绿、蓝三色的掩码,分别是: 0X7C00、0X03E0、0X001F。

BMP编码(从LCD读取图像数据)步骤如下:

1)创建BMP位图信息(上面的结构体),并初始化各个相关信息。         

首先,我们要设置BMP图片的分辨率为LCD分辨率、BMP图片的大小(整个BMP文件大小)、BMP的像素位数(16位)和掩码等信息。

2)创建新BMP文件,写入BMP位图信息。        

我们要保存BMP,当然要存放在某个地方(文件)(SD卡或U盘),所以需要先创建文件,同时先保存BMP位图信息,之后才开始BMP数据的写入。

3)保存位图数据。      

 这里就比较简单了,只需要从LCD的GRAM里面读取各点的颜色值,依次写入第二步创建的BMP文件即可。注意:保存顺序(即读GRAM顺序)是从左到右,从下到上

4)关闭文件。        

使用FATFS,在文件创建之后,必须调用f_close,文件才会真正体现在文件系统里面,否则是不会写入的!这个要特别注意,写完之后,一定要调用f_close。


JPEG编码简介

JPEG(Joint Photographic Experts Group)是一个由ISO和IEC两个组织机构联合组成的一个专家组,负责制定静态的数字图像数据压缩编码标准,这个专家组开发的算法称为JPEG算法,并且成为国际上通用的标准,因此又称为JPEG标准。        

JPEG是一个适用范围很广的静态图像数据压缩标准,既可用于灰度图像又可用于彩色图像。        

JPEG专家组开发了两种基本的压缩算法,一种是采用以离散余弦变换(Discrete Cosine Transform,DCT)为基础的有损压缩算法(使用较多),另一种是采用以预测技术为基础的无损压缩算法。使用有损压缩算法时,在压缩比为25:1的情况下,压缩后还原得到的图像与原始图像相比较,非图像专家难于找出它们之间的区别,因此得到了广泛的应用。

JPEG压缩是有损压缩,它利用了人的视角系统的特性,使用量化和无损压缩编码相结合来去掉视角的冗余信息和数据本身的冗余信息。

JPEG压缩编码分为三个步骤:

1)使用正向离散余弦变换(Forward Discrete Cosine Transform,FDCT)把空间域表示的图变换成频率域表示的图。        

2)使用加权函数对DCT系数进行量化,这个加权函数对于人的视觉系统是最佳的。        

3)使用霍夫曼可变字长编码器对量化系数进行编码。

JPEG(直接从摄像头读数据)拍照步骤如下:

1)初始化STM32F4的DCMI接口和OV2640模块。         

首先,我们要初始化STM32的DCMI接口(包括开启DMA和相关中断)和相关IO,然后配置好OV2640输出JPEG数据流。

2)读取OV2640模块的数据。        

在DCMI接口的驱动下,有序读取OV2640输出的JPEG数据流,我们采用DMA双缓冲来接收JPEG数据流,并将这些数据及时搬运到外部SRAM(不能直接将OV2640的数据输出到外部SRAM因为外部SRAM速度跟不上,通过DMA的传输完成中断来处理)。

3)保存JPEG数据。        

在采集完一帧JPEG数据后,利用fatfs,创建一个.jpg文件,然后将存储在外部SRAM的数组(以0XFF,0XD8开头)存储在这个文件里面,最后调用f_close关闭文件,即可实现JPEG拍照保存。


在摄像头实验里面,我们定义了一个很大的数组jpeg_buf(124KB)来存储JPEG图像数据,不过,本例程我们要用到内存管理,其他地方也要用到一些数组,所以,肯定无法再定义这么大的数组了。      

并且这个数组不能使用外部SRAM(实测:DCMI接口使用DMA直接传输JPEG数据到外部SRAM会出现数据丢失,所以DMA接收JPEG数据只能用内部SRAM),所以,本例程使用DMA的双缓冲机制来读取,DMA双缓冲读取JPEG数据框图如下图:

 DMA接收来自OV2640的JPEG数据流,首先使用M0AR(内存1)来存储,当M0AR满了以后,自动切换到M1AR(内存2),同时程序读取M0AR(内存1)的数据到外部SRAM;当M1AR满了以后,又切回M0AR,同时程序读取M1AR(内存2)的数据到外部SRAM;依次循环(此时的数据处理,是通过DMA传输完成中断实现的,在中断里面处理),直到帧中断,结束一帧数据的采集,读取剩余数据到外部SRAM,完成一次JPEG数据的采集。      

这里,M0AR,M1AR所指向的内存,必须是内部内存,不过由于采用了双缓冲机制,我们就不必定义一个很大的数组,一次性接收所有JPEG数据了,而是可以分批次接收,数组可以定义的比较小。        

最后,将存储在外部SRAM的jpeg数据,保存为.jpg/.jpeg存放在SD卡,就完成了一次JPEG拍照。

硬件连接

探索者STM32F407开发板,通过OLED/CAMERA接口与ATK-OV2640摄像头模块连接,硬件连接原理图如下:

有些引脚在使用时会发生冲突,我们要注意分时复用。

源码讲解 

PICTURE文件bmp.c中的BMP编码函数bmp_encode(u8 *filename,u16 x,u16 y,u16 width,u16 height,u8 mode),x、y是起始坐标width、height是截图的宽度和高度(不能超过屏幕),mode:模式.0,仅仅创建新文件的方式编码;1,如果之前存在文件,则覆盖之前的文件.如果没有,则创建新的文件,返回值:0,成功;其他,错误码。

//BMP编码函数
//将当前LCD屏幕的指定区域截图,存为16位格式的BMP文件 RGB565格式.
//保存为rgb565则需要掩码,需要利用原来的调色板位置增加掩码.这里我们已经增加了掩码.
//保存为rgb555格式则需要颜色转换,耗时间比较久,所以保存为565是最快速的办法.
//filename:存放路径
//x,y:在屏幕上的起始坐标  
//mode:模式.0,仅仅创建新文件的方式编码;1,如果之前存在文件,则覆盖之前的文件.如果没有,则创建新的文件.
//返回值:0,成功;其他,错误码.  
u8 bmp_encode(u8 *filename,u16 x,u16 y,u16 width,u16 height,u8 mode)
{				
	FIL* f_bmp;
	u16 bmpheadsize;			//bmp头大小	   	
 	BITMAPINFO hbmp;			//bmp头	 
	u8 res=0;
	u16 tx,ty;				   	//图像尺寸
	u16 *databuf;				//数据缓存区地址	   	
	u16 pixcnt;				   	//像素计数器
	u16 bi4width;		       	//水平像素字节数	   
	if(width==0||height==0)return PIC_WINDOW_ERR;	//区域错误
	if((x+width-1)>lcddev.width)return PIC_WINDOW_ERR;		//区域错误
	if((y+height-1)>lcddev.height)return PIC_WINDOW_ERR;	//区域错误 
 	 
#if BMP_USE_MALLOC == 1	//使用malloc	
	databuf=(u16*)pic_memalloc(1024);		//开辟至少bi4width大小的字节的内存区域 ,对240宽的屏,480个字节就够了.
	if(databuf==NULL)return PIC_MEM_ERR;		//内存申请失败.
	f_bmp=(FIL *)pic_memalloc(sizeof(FIL));	//开辟FIL字节的内存区域 
	if(f_bmp==NULL)								//内存申请失败.
	{		 
		pic_memfree(databuf);
		return PIC_MEM_ERR;				
	} 	 
#else
	databuf=(u16*)bmpreadbuf;
	f_bmp=&f_bfile;
#endif	      
	bmpheadsize=sizeof(hbmp);//得到bmp文件头的大小   
	mymemset((u8*)&hbmp,0,sizeof(hbmp));//置零空申请到的内存.	    
	hbmp.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);//信息头大小
	hbmp.bmiHeader.biWidth=width;	 	//bmp的宽度
	hbmp.bmiHeader.biHeight=height; 	//bmp的高度
	hbmp.bmiHeader.biPlanes=1;	 		//恒为1
	hbmp.bmiHeader.biBitCount=16;	 	//bmp为16位色bmp
	hbmp.bmiHeader.biCompression=BI_BITFIELDS;//每个象素的比特由指定的掩码决定。
 	hbmp.bmiHeader.biSizeImage=hbmp.bmiHeader.biHeight*hbmp.bmiHeader.biWidth*hbmp.bmiHeader.biBitCount/8;//bmp数据区大小
 				   
	hbmp.bmfHeader.bfType=((u16)'M'<<8)+'B';//BM格式标志
	hbmp.bmfHeader.bfSize=bmpheadsize+hbmp.bmiHeader.biSizeImage;//整个bmp的大小
   	hbmp.bmfHeader.bfOffBits=bmpheadsize;//到数据区的偏移

	hbmp.RGB_MASK[0]=0X00F800;	 		//红色掩码
	hbmp.RGB_MASK[1]=0X0007E0;	 		//绿色掩码
	hbmp.RGB_MASK[2]=0X00001F;	 		//蓝色掩码

	if(mode==1)res=f_open(f_bmp,(const TCHAR*)filename,FA_READ|FA_WRITE);//尝试打开之前的文件
 	if(mode==0||res==0x04)res=f_open(f_bmp,(const TCHAR*)filename,FA_WRITE|FA_CREATE_NEW);//模式0,或者尝试打开失败,则创建新文件		   
 	if((hbmp.bmiHeader.biWidth*2)%4)//水平像素(字节)不为4的倍数
	{
		bi4width=((hbmp.bmiHeader.biWidth*2)/4+1)*4;//实际要写入的宽度像素,必须为4的倍数.	
	}else bi4width=hbmp.bmiHeader.biWidth*2;		//刚好为4的倍数	 
 	if(res==FR_OK)//创建成功
	{
		res=f_write(f_bmp,(u8*)&hbmp,bmpheadsize,&bw);//写入BMP首部  
		for(ty=y+height-1;hbmp.bmiHeader.biHeight;ty--)
		{
			pixcnt=0;
 			for(tx=x;pixcnt!=(bi4width/2);)
			{
				if(pixcnt<hbmp.bmiHeader.biWidth)databuf[pixcnt]=LCD_ReadPoint(tx,ty);//读取坐标点的值 
				else databuf[pixcnt]=0Xffff;//补充白色的像素.  
				pixcnt++;
				tx++;
			}
			hbmp.bmiHeader.biHeight--;
			res=f_write(f_bmp,(u8*)databuf,bi4width,&bw);//写入数据
		}
		f_close(f_bmp);
	}	    
#if BMP_USE_MALLOC == 1	//使用malloc	
	pic_memfree(databuf);	 
	pic_memfree(f_bmp);		 
#endif	
	return res;
}

代码先判断高度宽度是否合法,不合法返回相应错误;然后看是否使用了内存管理,使用了申请内存,没有使用直接赋值给数组;然后设置结构体的相关初始值,根据mode创建文件;然后看水平像素字节数是否为4的倍数如果不是自动补齐;文件创建好后先写入BMP文件头然后从下到上从左到右从LCD中读取数据,读完一行后写入数据,最后保存文件。

HARDWARE文件dcmi.c中的DCMI_DMA_Init(u32 DMA_Memory0BaseAddr,u32 DMA_Memory1BaseAddr,u16 DMA_BufferSize,u32 DMA_MemoryDataSize,u32 DMA_MemoryInc)函数,参数依次是存储器1的地址、存储器2的地址、Buffer大小、存储器数据宽度、是否需要自增。函数前面的配置和摄像头的配置差不多,直接看下面的部分:

if(DMA_Memory1BaseAddr)
  {
		DMA_DoubleBufferModeCmd(DMA2_Stream1,ENABLE);//双缓冲模式
	    DMA_MemoryTargetConfig(DMA2_Stream1,DMA_Memory1BaseAddr,DMA_Memory_1);//配置目标地址1
		DMA_ITConfig(DMA2_Stream1,DMA_IT_TC,ENABLE);//开启传输完成中断
		
		NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream1_IRQn;
		NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;//抢占优先级0
		NVIC_InitStructure.NVIC_IRQChannelSubPriority =0;		//子优先级0
		NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
		NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器、
	}

DMA2_Stream1_IRQHandler(void)函数:

//DMA2_Stream1中断服务函数(仅双缓冲模式会用到)
void DMA2_Stream1_IRQHandler(void)
{        
	if(DMA_GetFlagStatus(DMA2_Stream1,DMA_FLAG_TCIF1)==SET)//DMA2_Steam1,传输完成标志
	{  
		 DMA_ClearFlag(DMA2_Stream1,DMA_FLAG_TCIF1);//清除传输完成中断
     dcmi_rx_callback();	//执行摄像头接收回调函数,读取数据等操作在这里面处理  
	}    											 
}

先判断中断类型是不是DMA2_Steam1传输完成中断,如果是先清除中断执行回调函数:

void (*dcmi_rx_callback)(void);//DCMI DMA接收回调函数

这个函数在main.c中初始化:

dcmi_rx_callback=jpeg_dcmi_rx_callback;//回调函数

//jpeg数据接收回调函数
void jpeg_dcmi_rx_callback(void)
{ 
	u16 i;
	u32 *pbuf;
	pbuf=jpeg_data_buf+jpeg_data_len;//偏移到有效数据末尾
	if(DMA2_Stream1->CR&(1<<19))//buf0已满,正常处理buf1
	{ 
		for(i=0;i<jpeg_dma_bufsize;i++)pbuf[i]=jpeg_buf0[i];//读取buf0里面的数据
		jpeg_data_len+=jpeg_dma_bufsize;//偏移
	}else //buf1已满,正常处理buf0
	{
		for(i=0;i<jpeg_dma_bufsize;i++)pbuf[i]=jpeg_buf1[i];//读取buf1里面的数据
		jpeg_data_len+=jpeg_dma_bufsize;//偏移 
	} 	
}

这个函数实现数据搬运,当一个内存区传输满后,先把buf偏移到有效数据末尾,然后看那个已满就把数据搬运到外部。

main.c文件的帧中断处理JPEG数据jpeg_data_process(void)函数:

//处理JPEG数据
//当采集完一帧JPEG数据后,调用此函数,切换JPEG BUF.开始下一帧采集.
void jpeg_data_process(void)
{
	u16 i;
	u16 rlen;//剩余数据长度
	u32 *pbuf;
	if(ov2640_mode)//只有在JPEG格式下,才需要做处理.
	{
		if(jpeg_data_ok==0)	//jpeg数据还未采集完?
		{
			DMA_Cmd(DMA2_Stream1,DISABLE);		//停止当前传输
			while(DMA_GetCmdStatus(DMA2_Stream1) != DISABLE);	//等待DMA2_Stream1可配置 
			rlen=jpeg_dma_bufsize-DMA_GetCurrDataCounter(DMA2_Stream1);//得到剩余数据长度	
			pbuf=jpeg_data_buf+jpeg_data_len;//偏移到有效数据末尾,继续添加
			if(DMA2_Stream1->CR&(1<<19))for(i=0;i<rlen;i++)pbuf[i]=jpeg_buf1[i];//读取buf1里面的剩余数据
			else for(i=0;i<rlen;i++)pbuf[i]=jpeg_buf0[i];//读取buf0里面的剩余数据 
			jpeg_data_len+=rlen;			//加上剩余长度
			jpeg_data_ok=1; 				//标记JPEG数据采集完按成,等待其他函数处理
		}
		if(jpeg_data_ok==2)	//上一次的jpeg数据已经被处理了
		{ DMA_SetCurrDataCounter(DMA2_Stream1,jpeg_dma_bufsize);//传输长度为jpeg_buf_size*4字节
			DMA_Cmd(DMA2_Stream1,ENABLE); //重新传输
			jpeg_data_ok=0;					//标记数据未采集
			jpeg_data_len=0;				//数据重新开始
		}
	}
    else
    {
        LCD_SetCursor(0, 0);
        LCD_WriteRAM_Prepare();     //开始写入GRAM
        hsync_int = 1;
    }
}

main函数,先进行各种的配置然后在SD卡中创建文件夹,然后申请内存,初始化OV2640,配置JPEG、DCMI,最后根据按键进行操作。

 	//main中的部分代码
    while(1)
	{	
		key=KEY_Scan(0);//不支持连按
		if(key)
		{
			DCMI_Stop(); //停止显示 
			if(key==WKUP_PRES) 
			{
				scale=!scale;  
				if(scale==0)
				{
                    if(lcddev.id == 0X5510)
                    {
                        SCCB_WR_Reg(0xd3,0x02);
                    }
					OV2640_ImageWin_Set((1600-lcddev.width)/2,(1200-lcddev.height)/2,lcddev.width,lcddev.height);//1:1真实尺寸
					OV2640_OutSize_Set(lcddev.width,lcddev.height); 
					sprintf((char*)msgbuf,"Full Size 1:1");
				}else 
				{
					OV2640_ImageWin_Set(0,0,1600,1200);				//全尺寸缩放
					OV2640_OutSize_Set(lcddev.width,lcddev.height); 
					sprintf((char*)msgbuf,"Scale");
				}
				LCD_ShowString(30,50,210,16,16,msgbuf);//显示提示内容
				delay_ms(800); 	
			}else if(sd_ok)//SD卡正常才可以拍照
			{    
				sw_sdcard_mode();	//切换为SD卡模式
				if(key==KEY0_PRES)	//BMP拍照
				{
					camera_new_pathname(pname,0);//得到文件名	
					res=bmp_encode(pname,0,0,lcddev.width,lcddev.height,0);
				}else if(key==KEY1_PRES)//JPG拍照
				{
					camera_new_pathname(pname,1);//得到文件名	
					res=ov2640_jpg_photo(pname);
					if(scale==0)
					{
						OV2640_ImageWin_Set((1600-lcddev.width)/2,(1200-lcddev.height)/2,lcddev.width,lcddev.height);//1:1真实尺寸
						OV2640_OutSize_Set(lcddev.width,lcddev.height); 
					}else 
					{
						OV2640_ImageWin_Set(0,0,1600,1200);	//全尺寸缩放 
					}
					OV2640_OutSize_Set(lcddev.width,lcddev.height); 					
				}
				sw_ov2640_mode();	//切换为OV2640模式
				if(res)//拍照有误
				{
					Show_Str(30,130,240,16,"写入文件错误!",16,0);		 
				}else 
				{
					Show_Str(30,130,240,16,"拍照成功!",16,0);
					Show_Str(30,150,240,16,"保存为:",16,0);
					Show_Str(30+42,150,240,16,pname,16,0);		    
					BEEP=1;	//蜂鸣器短叫,提示拍照完成
					delay_ms(100);
				}   			
			}else //提示SD卡错误
			{					    
				Show_Str(30,130,240,16,"SD卡错误!",16,0);
				Show_Str(30,150,240,16,"拍照功能不可用!",16,0);			    
			}   	
			BEEP=0;			//关闭蜂鸣器 
			if(key!=WKUP_PRES)delay_ms(1800);//非尺寸切换,等待1.8秒钟
			DCMI_Start(); 	//停止显示  
		} 
        if (hsync_int)  //刚刚产生帧中断,可以延时
        {
            delay_ms(10);
            hsync_int = 0;
        }
		i++;
		if(i==20)//DS0闪烁.
		{
			i=0;
			LED0=!LED0;
 		}
	}

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值