1)實驗平台:正點原子MiniPro H750開發闆
2)平台購買位址:https://detail.tmall.com/item.htm?id=677017430560
3)全套實驗源碼+手冊+視訊下載下傳位址:http://www.openedv.com/thread-336836-1-1.html
4)對正點原子STM32感興趣的同學可以加群讨論:879133275
第五十章 照相機實驗
上一章,我們學習了如何使用STM32H750自帶的硬體JPEG編解碼器,實作對JPG/JPEG圖檔的硬解碼,進而大大提高解碼速度。本章我們将學習BMP&JPEG編碼,結合前面的攝像頭實驗,實作一個簡單的照相機。
本章分為如下幾個小節:
50.1 BMP&JPEG編碼簡介
50.2 硬體設計
50.3 程式設計
50.4 下載下傳驗證
50.1 BMP&JPEG編碼簡介
我們要實作支援BMP圖檔格式的照片和JPEG圖檔格式的照片的照相機功能,這裡簡單介紹一下這兩種圖檔格式的編碼。這裡我們使用ATK-OV5640-AF攝像頭,來實作拍照。關于OV5640的相關知識點,請參考第四十三章。
50.1.1 BMP編碼簡介
前面的章節中,我們學習了各種圖檔格式的解碼。本章,我們介紹最簡單的圖檔編碼方法:BMP圖檔編碼。通過前面的了解,我們知道BMP檔案是由檔案頭、位圖資訊頭、顔色資訊和圖形資料等四部分組成。我們先來了解下這幾個部分。
1、BMP檔案頭(14位元組):BMP檔案頭資料結構含有BMP檔案的類型、檔案大小和位圖起始位置等資訊。
/* BMP頭檔案 */
typedef __packed struct
{
uint16_t bfType ; /* 檔案标志.隻對'BM',用來識别BMP位圖類型 */
uint32_t bfSize ; /* 檔案大小,占四個位元組 */
uint16_t bfReserved1 ; /* 保留 */
uint16_t bfReserved2 ; /* 保留 */
uint32_t bfOffBits ; /* 從檔案開始到位圖資料(bitmap data)開始之間的的偏移量 */
}BITMAPFILEHEADER ;
2、位圖資訊頭(40位元組):BMP位圖資訊頭資料用于說明位圖的尺寸等資訊。
typedef __packed struct
{
uint32_t biSize ; /* 說明BITMAPINFOHEADER結構所需要的字數。 */
long biWidth ; /* 說明圖象的寬度,以象素為機關 */
long biHeight ; /* 說明圖象的高度,以象素為機關 */
uint16_t biPlanes ; /* 為目标裝置說明位面數,其值将總是被設為1 */
uint16_t biBitCount ; /* 說明比特數/象素,其值為1、4、8、16、24、或32 */
uint32_t biCompression ;/* 說明圖象資料壓縮的類型。其值可以是下述值之一
* BI_RGB :沒有壓縮
* BI_RLE8 :每個象素8比特的RLE壓縮編碼,壓縮格式由
2位元組組成(重複象素計數和顔色索引)
* BI_RLE4 :每個象素4比特的RLE壓縮編碼,壓縮格式由
2位元組組成
* BI_BITFIELDS:每個象素的比特由指定的掩碼決定
*/
uint32_t biSizeImage ;/*說明圖象的大小,以位元組為機關。當用BI_RGB格式時,可設定為0*/
long biXPelsPerMeter ; /* 說明水準分辨率,用象素/米表示 */
long biYPelsPerMeter ; /* 說明垂直分辨率,用象素/米表示 */
uint32_t biClrUsed ; /* 說明位圖實際使用的彩色表中的顔色索引數 */
/* 說明對圖象顯示有重要影響的顔色索引的數目,如果是0,表示都重要 */
uint32_t biClrImportant ;
}BITMAPINFOHEADER ;
3、顔色表:顔色表用于說明位圖中的顔色,它有若幹個表項,每一個表項是一個RGBQUAD類型的結構,定義一種顔色。
typedef __packed struct
{
uint8_t rgbBlue ; /* 指定藍色強度 */
uint8_t rgbGreen ; /* 指定綠色強度 */
uint8_t rgbRed ; /* 指定紅色強度 */
uint8_t rgbReserved ; /* 保留,設定為0 */
}RGBQUAD ;
顔色表中RGBQUAD結構資料的個數由biBitCount來确定:當biBitCount=1、4、8時,分别有2、16、256個表項;當biBitCount大于8時,沒有顔色表項。
BMP檔案頭、位圖資訊頭和顔色表組成位圖資訊(我們将BMP檔案頭也加進來,友善處理),BITMAPINFO結構定義如下:
typedef __packed struct
{
BITMAPFILEHEADER bmfHeader;
BITMAPINFOHEADER bmiHeader;
uint32_t RGB_MASK[3]; /* 調色闆用于存放RGB掩碼 */
//RGBQUAD bmiColors[256];
}BITMAPINFO;
4、位圖資料:位圖資料記錄了位圖的每一個像素值,記錄順序是在掃描行内是從左到右,掃描行之間是從下到上。位圖的一個像素值所占的位元組數:
當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=1 表示位圖最多有兩種顔色,預設情況下是黑色和白色,你也可以自己定義這兩種顔色。圖像資訊頭裝調色闆中将有兩個調色闆項,稱為索引0和索引1。圖象資料陣列中的每一位表示一個像素。如果一個位是0,顯示時就使用索引0的RGB值,如果位是1,則使用索引1的RGB值。
biBitCount=16 表示位圖最多有65536種顔色。每個像素用16位(2個位元組)表示。這種格式叫作高彩色,或叫增強型16位色,或64K色。它的情況比較複雜,當biCompression成員的值是BI_RGB時,它沒有調色闆。16位中,最低的5位表示藍色分量,中間的5位表示綠色分量,高的5位表示紅色分量,一共占用了15位,最高的一位保留,設為0。這種格式也被稱作555 16位位圖。如果biCompression成員的值是BI_BITFIELDS,那麼情況就複雜了,首先是原來調色闆的位置被三個DWORD變量占據,稱為紅、綠、藍掩碼。分别用于描述紅、綠、藍分量在16位中所占的位置。在Windows 95(或98)中,系統可接受兩種格式的位域:555和565,在555格式下,紅、綠、藍的掩碼分别是:0x7C00、0x03E0、0x001F,而在565格式下,它們則分别為:0xF800、0x07E0、0x001F。你在讀取一個像素之後,可以分别用掩碼“與”上像素值,進而提取出想要的顔色分量(當然還要再經過适當的左右移操作)。在NT系統中,則沒有格式限制,隻不過要求掩碼之間不能有重疊。(注:這種格式的圖像使用起來是比較麻煩的,不過因為它的顯示效果接近于真彩,而圖像資料又比真彩圖像小的多,是以,它更多的被用于遊戲軟體)。
biBitCount=32 表示位圖最多有4294967296(2的32次方)種顔色。這種位圖的結構與16位位圖結構非常類似,當biCompression成員的值是BI_RGB時,它也沒有調色闆,32位中有24位用于存放RGB值,順序是:最高位—保留,紅8位、綠8位、藍8位。這種格式也被成為888 32位圖。如果 biCompression成員的值是BI_BITFIELDS時,原來調色闆的位置将被三個DWORD變量占據,成為紅、綠、藍掩碼,分别用于描述紅、綠、藍分量在32位中所占的位置。在Windows 95(or 98)中,系統隻接受888格式,也就是說三個掩碼的值将隻能是:0xFF0000、0xFF00、0xFF。而NT系統,隻要注意使掩碼之間不産生重疊就行。(注:這種圖像格式比較規整,因為它是DWORD對齊的,是以在記憶體中進行圖像處理時可進行彙編級的代碼優化(簡單)。
通過以上了解,我們對BMP有了一個比較深入的了解,本章,我們采用16位BMP編碼(因為我們的LCD就是16位色的,而且16位BMP編碼比24位BMP編碼更省空間),故我們需要設定biBitCount的值為16,這樣得到新的位圖資訊(BITMAPINFO)結構體:
typedef __packed struct
{
BITMAPFILEHEADER bmfHeader;
BITMAPINFOHEADER bmiHeader;
uint32_t RGB_MASK[3]; /* 調色闆用于存放RGB掩碼 */
}BITMAPINFO;
其實就是顔色表由3個RGB掩碼代替。最後,我們來看看将LCD的顯存儲存為BMP格式的圖檔檔案的步驟:
1)建立BMP位圖資訊,并初始化各個相關資訊
這裡,我們要設定BMP圖檔的分辨率為LCD分辨率、BMP圖檔的大小(整個BMP檔案大小)、BMP的像素位數(16位)和掩碼等資訊。
2)建立新BMP檔案,寫入BMP位圖資訊
我們要儲存BMP,當然要存放在某個地方(檔案),是以需要先建立檔案,同時先儲存BMP位圖資訊,之後才開始BMP資料的寫入。
3)儲存位圖資料。
這裡就比較簡單了,隻需要從LCD的GRAM裡面讀取各點的顔色值,依次寫入第二步建立的BMP檔案即可。注意:儲存順序(即讀GRAM順序)是從左到右,從下到上。
4)關閉檔案。
使用FATFS,在檔案建立之後,必須調用f_close,檔案才會真正展現在檔案系統裡面,否則是不會寫入的!這個要特别注意,寫完之後,一定要調用f_close。
BMP編碼就介紹到這裡。
50.1.2 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壓縮的過程了,大家可以自行查找相關資料。我們本實驗要實作的JPEG拍照,并不需要自己壓縮圖像,因為我們使用的ALIENTEK OV5640攝像頭子產品,直接就可以輸出壓縮後的JPEG資料,我們完全不需要理會壓縮過程,是以本實驗我們實作JPEG拍照的關鍵,在于準确接收OV5640攝像頭子產品發送過來的編碼資料,然後将這些資料儲存為.jpg檔案,就可以實作JPEG拍照了。
在第四十三章的攝像頭實驗中,我們定義了一個很大的數組jpeg_data_buf(480KB位元組)來存儲JPEG圖像資料。而在本實驗中,我們可以使用記憶體管理來申請記憶體,無需定義這麼大的數組,使用上更加靈活。DCMI接口使用DMA直接傳輸JPEG資料,DMA接收到的JPEG資料放到内部SRAM。是以,我們本章将使用DMA的雙緩沖機制來讀取,DMA雙緩沖讀取JPEG資料框圖如圖50.1.2.1所示:
圖50.1.2.1 DMA雙緩沖讀取JPEG資料原理框圖
DMA接收來自OV5640的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拍照。
50.2 硬體設計
-
例程功能
1、首先是檢測字庫,然後檢測SD卡根目錄是否存在PHOTO檔案夾,如果不存在則建立,如果建立失敗,則報錯(提示拍照功能不可用)。在找到SD卡的PHOTO檔案夾後,開始初始化OV5640,如果初始化成功,則提示資訊:KEY0:拍照(bmp格式),KEY1:拍照(jpg格式),WK_UP選擇:1:1顯示,即不縮放,圖檔不變形,但是顯示區域小(液晶分辨率大小),或者縮放顯示,即将1280*800的圖像壓縮到液晶分辨率尺寸顯示,圖檔變形,但是顯示了整個圖檔内容。可以通過序列槽1,借助USMART設定/讀取OV5640的寄存器,友善大家調試。
2、LED0閃爍,提示程式運作。LED1用于訓示幀中斷。
-
硬體資源
1)RGB燈 RED :LED0 - PB4 GREEN :LED1 - PE6
2)序列槽1(PA9/PA10連接配接在闆載USB轉序列槽晶片CH340上面)
3)正點原子2.8/3.5/4.3/7/10寸TFTLCD子產品(僅限MCU屏,16位8080并口驅動)
4)獨立按鍵 :KEY0 - PA1、KEY1 - PA15、WK_UP - PA0
5)SD卡,通過SDMMC1(SDMMC_D0D4(PC8PC11),
SDMMC_SCK(PC12),SDMMC_CMD(PD2))連接配接
6)norflash(QSPI FLASH晶片,連接配接在QSPI上)
7)硬體JPEG解碼核心(STM32H750自帶)
8)定時器6(用于列印攝像頭幀率等資訊)
9)ALIENTEK OV5640攝像頭子產品,連接配接關系為:
OV5640子產品 ----------- STM32開發闆
OV_D0~D7 ------------ PC6/PC7/PC8/PC9/PC11/PD3/PB8/PB9
OV_SCL ------------ PB10
OV_SDA ------------ PB11
OV_VSYNC ------------ PB7
OV_HREF ------------ PA4
OV_RESET ------------ PA7
OV_PCLK ------------ PA6
OV_PWDN ------------ PC4
50.3 程式設計
50.3.1 程式流程圖
圖50.3.1.1 照相機實驗程式流程圖
50.3.2 程式解析
-
PICTURE驅動代碼
這裡我們隻講解核心代碼,詳細的源碼請大家參考CD光牒本實驗對應源碼。PICTURE驅動源碼包括兩個檔案:bmp.c和bmp.h。
bmp.h頭檔案在50.1.1小節基本講過,具體請看源碼。下面來看到bmp.c檔案裡面的bmp編碼函數:bmp_encode,該函數代碼如下:
/**
* @brief BMP編碼函數
* @note 将目前LCD螢幕的指定區域截圖,存為16位格式的BMP檔案 RGB565格式.
* 儲存為rgb565則需要掩碼,需要利用原來的調色闆位置增加掩碼.這裡我們已經增加了掩碼.
* 儲存為rgb555格式則需要顔色轉換,耗時間比較久,是以儲存為565是最快速的辦法.
*
* @param filename : 包含存儲路徑的檔案名(.bmp)
* @param x, y : 起始坐标
* @param width,height: 區域大小
* @param acolor : 附加的alphablend的顔色(這個僅對32位色bmp有效!!!)
* @param mode : 儲存模式
* @arg 0, 僅僅建立新檔案的方式編碼;
* @arg 1, 如果之前存在檔案,則覆寫之前的檔案.如果沒有,則建立新的檔案;
* @retval 操作結果
* @arg 0 , 成功
* @arg 其他, 錯誤碼
*/
uint8_t bmp_encode(uint8_t *filename, uint16_t x, uint16_t y, uint16_t width,
uint16_t height, uint8_t mode)
{
FIL *f_bmp;
uint32_t bw = 0;
uint16_t bmpheadsize; /* bmp頭大小 */
BITMAPINFO hbmp; /* bmp頭 */
uint8_t res = 0;
uint16_t tx, ty; /* 圖像尺寸 */
uint16_t *databuf; /* 資料緩存區位址 */
uint16_t pixcnt; /* 像素計數器 */
uint16_t 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 */
/* 開辟至少bi4width大小的位元組的記憶體區域 ,對240寬的屏,480個位元組就夠了.
最大支援1024寬度的bmp編碼 */
databuf = (uint16_t *)piclib_mem_malloc(2048);
if (databuf == NULL)return PIC_MEM_ERR; /* 記憶體申請失敗. */
f_bmp = (FIL *)piclib_mem_malloc(sizeof(FIL)); /* 開辟FIL位元組的記憶體區域 */
if (f_bmp == NULL) /* 記憶體申請失敗 */
{
piclib_mem_free(databuf);
return PIC_MEM_ERR;
}
#else
databuf = (uint16_t *)bmpreadbuf;
f_bmp = &f_bfile;
#endif
bmpheadsize = sizeof(hbmp); /* 得到bmp檔案頭的大小 */
my_mem_set((uint8_t *)&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 = ((uint16_t)'M' << 8) + 'B'; /* BM格式标志 */
/* 整個bmp的大小 */
hbmp.bmfHeader.bfSize = bmpheadsize + hbmp.bmiHeader.biSizeImage;
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)
{
/* 模式0,或者嘗試打開失敗,則建立新檔案 */
res = f_open(f_bmp, (const TCHAR *)filename, FA_WRITE | FA_CREATE_NEW);
}
if ((hbmp.bmiHeader.biWidth * 2) % 4) /* 水準像素(位元組)不為4的倍數 */
{
/* 實際要寫入的寬度像素,必須為4的倍數 */
bi4width = ((hbmp.bmiHeader.biWidth * 2) / 4 + 1) * 4;
}
else
{
bi4width = hbmp.bmiHeader.biWidth * 2; /* 剛好為4的倍數 */
}
if (res == FR_OK) /* 建立成功 */
{
res = f_write(f_bmp, (uint8_t *)&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] = pic_phy.read_point(tx, ty);/* 讀取坐标點的值 */
}
else
{
databuf[pixcnt] = 0Xffff; /* 補充白色的像素 */
}
pixcnt++;
tx++;
}
hbmp.bmiHeader.biHeight--;
res = f_write(f_bmp, (uint8_t *)databuf, bi4width, &bw);/* 寫入資料 */
}
f_close(f_bmp);
}
#if BMP_USE_MALLOC == 1 /* 使用malloc */
piclib_mem_free(databuf);
piclib_mem_free(f_bmp);
#endif
return res;
}
該函數實作了對LCD螢幕的任意指定區域進行截屏儲存,用到的方法就是50.1.1節我們所介紹的方法,該函數實作了将LCD任意指定區域的内容,儲存個為16位BMP格式,存放在指定位置(由filename決定)。注意,代碼中的BMP_USE_MALLOC是在bmp.h定義的一個宏,用于設定是否使用malloc,本章我們選擇使用malloc。
2. main.c代碼
main.c前面定義了一些變量和數組,具體如下:
volatile uint8_t g_bmp_request = 0;
uint8_t g_ovx_mode = 0; /* bit0:0,RGB565模式;1,JPEG模式 */
uint16_t g_curline = 0; /* 攝像頭輸出資料,目前行編号 */
uint16_t g_yoffset = 0; /* y方向的偏移量 */
#define jpeg_buf_size 440*1024 /* 定義JPEG資料緩存jpeg_buf的大小(440K位元組) */
#define jpeg_line_size 2*1024 /* 定義DMA接收資料時,一行資料的最大值 */
uint32_t *p_dcmi_line_buf[2]; /* RGB屏時,攝像頭采用一行一行讀取,定義行緩存 */
uint32_t *p_jpeg_data_buf; /* JPEG資料緩存buf */
volatile uint32_t g_jpeg_data_len = 0; /* buf中的JPEG有效資料長度 */
volatile uint8_t g_jpeg_data_ok = 0; /* JPEG資料采集完成标志
* 0,資料沒有采集完;
* 1,資料采集完了,但是還沒處理;
* 2,資料已經處理完成了,可以開始下一幀接收
*/
在main.c裡面,總共有7個函數,我們接下來分别介紹。首先是處理JPEG資料函數,其定義如下:
/**
* @brief 處理JPEG資料
* @note 當采集完一幀JPEG資料後,調用此函數,切換JPEG BUF.開始下一幀采集
* @param 無
* @retval 無
*/
void jpeg_data_process(void)
{
uint16_t i;
uint16_t rlen; /* 剩餘資料長度 */
uint32_t *pbuf;
g_curline = g_yoffset; /* 行數複位 */
if (g_ovx_mode & 0X01) /* 隻有在JPEG格式下,才需要做處理 */
{
if (g_jpeg_data_ok == 0) /* jpeg資料還未采集完 */
{
__HAL_DMA_DISABLE(&g_dma_dcmi_handle); /* 停止目前傳輸 */
/* 得到剩餘資料長度 */
rlen=jpeg_line_size-__HAL_DMA_GET_COUNTER(&g_dma_dcmi_handle);
/* 記憶體不夠了,直接退出 */
if (g_jpeg_data_len > (jpeg_buf_size / 4 - rlen))
{
/* 列印目前長度(uint32_t)*/
printf("g_jpeg_data_len1:%d\r\n", g_jpeg_data_len);
g_jpeg_data_ok = 1; /* 标記JPEG資料采集完按成,等待其他函數處理 */
return;
}
pbuf = p_jpeg_data_buf + g_jpeg_data_len;/* 偏移到有效資料末尾,繼續添加 */
if (DMA1_Stream1->CR & (1 << 19))
{
for (i = 0; i < rlen; i++)
{
pbuf[i] = p_dcmi_line_buf[1][i]; /* 讀取buf1裡面的剩餘資料 */
}
}
else
{
for (i = 0; i < rlen; i++)
{
pbuf[i] = p_dcmi_line_buf[0][i]; /* 讀取buf0裡面的剩餘資料 */
}
}
g_jpeg_data_len += rlen; /* 加上剩餘長度 */
g_jpeg_data_ok = 1; /* 标記JPEG資料采集完按成,等待其他函數處理 */
}
if (g_jpeg_data_ok == 2) /* 上一次的jpeg資料已經被處理了 */
{
/* 傳輸長度為jpeg_buf_size*4位元組 */
__HAL_DMA_SET_COUNTER(&g_dma_dcmi_handle, jpeg_line_size);
__HAL_DMA_ENABLE(&g_dma_dcmi_handle); /* 重新傳輸 */
g_jpeg_data_ok = 0; /* 标記資料未采集 */
g_jpeg_data_len = 0; /* 資料重新開始 */
}
}
else
{
if (g_bmp_request == 1) /* 有bmp拍照請求,關閉DCMI */
{
dcmi_stop(); /* 停止DCMI */
g_bmp_request = 0; /* 标記請求處理完成 */
}
lcd_set_cursor(0, 0);
lcd_write_ram_prepare(); /* 開始寫入GRAM */
}
}
該函數用于處理JPEG資料的接收,在DCMI_IRQHandler函數(在dcmi.c裡面)裡面被調用,它與jpeg_dcmi_rx_callback函數和ov5640_jpg_photo函數共同控制JPEG的資料的采集。JPEG資料的接收,采用DMA雙緩沖機制,緩沖數組為:p_dcmi_line_buf(u32類型,RGB屏接收RGB565資料時,也是用這個數組);數組大小為:jpeg_line_size,我們定義的是2*1024,即數組大小為8K位元組(數組大小不能小于存儲攝像頭一行輸出資料的大小);JPEG資料接收處理流程就是按圖50.1.2.1所示流程來實作的。由DMA傳輸完成中斷和DCMI幀中斷,兩個中斷服務函數共同完成jpeg資料的采集。采集到的JPEG資料,全部存儲在p_jpeg_data_buf數組裡面,p_jpeg_data_buf數組采用記憶體管理,從内部SRAM申請440K記憶體作為JPEG資料的緩存。
接下來介紹的是JPEG資料接收回調函數,其定義如下:
/**
* @brief JPEG資料接收回調函數
* @param 無
* @retval 無
*/
void jpeg_dcmi_rx_callback(void)
{
uint16_t i;
volatile uint32_t *pbuf;
/* 記憶體不夠了,直接退出 */
if (g_jpeg_data_len > (jpeg_buf_size / 4 - jpeg_line_size))
{
/* 列印目前長度(uint32_t) */
printf("g_jpeg_data_len:%d\r\n", g_jpeg_data_len);
return;
}
pbuf = p_jpeg_data_buf + g_jpeg_data_len; /* 偏移到有效資料末尾 */
if (DMA1_Stream1->CR & (1 << 19)) /* buf0已滿,正常處理buf1 */
{
for (i = 0; i < jpeg_line_size; i++)
{
pbuf[i] = p_dcmi_line_buf[0][i]; /* 讀取buf0裡面的資料 */
}
g_jpeg_data_len += jpeg_line_size; /* 偏移 */
}
else /* buf1已滿,正常處理buf0 */
{
for (i = 0; i < jpeg_line_size; i++)
{
pbuf[i] = p_dcmi_line_buf[1][i]; /* 讀取buf1裡面的資料 */
}
g_jpeg_data_len += jpeg_line_size; /* 偏移 */
}
SCB_CleanInvalidateDCache(); /* 清除無效化DCache */
}
這是jpeg資料接收的主要函數,通過判斷DMA1_Stream1->CR寄存器,讀取不同p_dcmi_line_buf裡面的資料,存儲到SRAM裡面(p_jpeg_data_buf)。該函數由DMA的傳輸完成中斷服務函數:DMA1_Stream1_IRQHandler調用。
接下來介紹的是切換為OV5640模式函數,其定義如下:
/**
* @brief 切換為OV5640模式
* @note 切換PC8/PC9/PC11為DCMI複用功能(AF13)
* @param 無
* @retval 無
*/
void sw_ov5640_mode(void)
{
GPIO_InitTypeDef gpio_init_struct;
ov5640_write_reg(0X3017, 0XFF); /* 開啟OV5650輸出(可以正常顯示) */
ov5640_write_reg(0X3018, 0XFF);
/* GPIOC8/9/11切換為 DCMI接口 */
gpio_init_struct.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_11;
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 推挽複用 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;/* 高速 */
gpio_init_struct.Alternate = GPIO_AF13_DCMI; /* 複用為DCMI */
HAL_GPIO_Init(GPIOC, &gpio_init_struct); /* 初始化PC8,9, 11引腳 */
}
因為SD卡和OV5640有幾個IO共用,是以這幾個IO需要分時複用。該函數用于切換GPIO8/9/11的複用功能為DCMI接口,并開啟OV5640,這樣攝像頭子產品,可以開始正常工作。
接下來介紹的是切換為SD卡模式函數,其定義如下:
/**
* @brief 切換為SD卡模式
* @note 切換PC8/PC9/PC11為SDMMC複用功能(AF12)
* @param 無
* @retval 無
*/
void sw_sdcard_mode(void)
{
GPIO_InitTypeDef gpio_init_struct;
ov5640_write_reg(0X3017, 0X00); /* 關閉OV5640全部輸出(不影響SD卡通信) */
ov5640_write_reg(0X3018, 0X00);
/* GPIOC8/9/11切換為 SDIO接口 */
gpio_init_struct.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_11;
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 推挽複用 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;/* 高速 */
gpio_init_struct.Alternate = GPIO_AF12_SDIO1; /* 複用為SDIO */
HAL_GPIO_Init(GPIOC, &gpio_init_struct); /* 初始化PC8,9, 11引腳 */
}
該數用于切換GPIO8/9/11的複用功能為SDMMC接口,并關閉OV5640,這樣,SD卡可以開始正常工作。
接下來介紹的是檔案名自增(避免覆寫)函數,其定義如下:
/**
* @brief 檔案名自增(避免覆寫)
* @note bmp組合成: 形如 "0:PHOTO/PIC13141.bmp" 的檔案名
* jpg組合成: 形如 "0:PHOTO/PIC13141.jpg" 的檔案名
* @param pname : 有效的檔案名
* @param mode : 0, 建立.bmp檔案; 1, 建立.jpg檔案;
* @retval 無
*/
void camera_new_pathname(uint8_t *pname, uint8_t mode)
{
uint8_t res;
uint16_t index = 0;
FIL *ftemp;
ftemp = (FIL *)mymalloc(SRAMIN, sizeof(FIL)); /* 開辟FIL位元組的記憶體區域 */
if (ftemp == NULL) return; /* 記憶體申請失敗 */
while (index < 0XFFFF)
{
if (mode == 0) /* 建立.bmp檔案名 */
{
sprintf((char *)pname, "0:PHOTO/PIC%05d.bmp", index);
}
else /* 建立.jpg檔案名 */
{
sprintf((char *)pname, "0:PHOTO/PIC%05d.jpg", index);
}
res = f_open(ftemp, (const TCHAR *)pname, FA_READ); /* 嘗試打開這個檔案 */
if (res == FR_NO_FILE)break; /* 該檔案名不存在, 正是我們需要的 */
index++;
}
myfree(SRAMIN, ftemp);
}
該函數用于生成新的帶路徑的檔案名,且不會重複,防止檔案互相覆寫。該函數可以生成.bmp/.jpg的檔案名,友善拍照的時候,儲存到SD卡裡面。
接下來介紹的是OV5640拍照jpg圖檔函數,其定義如下:
/**
* @brief OV5640拍照jpg圖檔
* @param pname : 要建立的jpg檔案名(含路徑)
* @retval 0, 成功; 其他,錯誤代碼;
*/
uint8_t ov5640_jpg_photo(uint8_t *pname)
{
FIL *f_jpg;
uint8_t res = 0, headok = 0;
uint32_t bwr;
uint32_t i, jpgstart, jpglen;
uint8_t *pbuf;
f_jpg = (FIL *)mymalloc(SRAMIN, sizeof(FIL)); /* 開辟FIL位元組的記憶體區域 */
if (f_jpg == NULL)return 0XFF; /* 記憶體申請失敗 */
g_ovx_mode = 1;
g_jpeg_data_ok = 0;
sw_ov5640_mode(); /* 切換為OV5640模式 */
ov5640_jpeg_mode(); /* JPEG模式 */
ov5640_outsize_set(4, 0, 1280, 800); /* 設定輸出尺寸(WXGA) */
dcmi_rx_callback = jpeg_dcmi_rx_callback; /* JPEG接收資料回調函數 */
dcmi_dma_init((uint32_t)p_dcmi_line_buf[0], (uint32_t)p_dcmi_line_buf[1],
jpeg_line_size, DMA_MDATAALIGN_WORD, DMA_MINC_ENABLE); /* DCMI DMA配置 */
dcmi_start(); /* 啟動傳輸 */
while (g_jpeg_data_ok != 1); /* 等待第一幀圖檔采集完 */
g_jpeg_data_ok = 2; /* 忽略本幀圖檔,啟動下一幀采集 */
while (g_jpeg_data_ok != 1); /* 等待第二幀圖檔采集完,第二幀,才儲存到SD卡去 */
dcmi_stop(); /* 停止DMA搬運 */
g_ovx_mode = 0;
sw_sdcard_mode(); /* 切換為SD卡模式 */
printf("jpeg data size:%d\r\n", g_jpeg_data_len * 4);/*序列槽列印JPEG檔案大小 */
pbuf = (uint8_t *)p_jpeg_data_buf;
jpglen = 0; /* 設定jpg檔案大小為0 */
headok = 0; /* 清除jpg頭标記 */
/* 查找0XFF,0XD8和0XFF,0XD9,擷取jpg檔案大小 */
for (i = 0; i < g_jpeg_data_len * 4; i++)
{
if ((pbuf[i] == 0XFF) && (pbuf[i + 1] == 0XD8)) /* 找到FF D8 */
{
jpgstart = i;
headok = 1; /* 标記找到jpg頭(FF D8) */
}
/* 找到頭以後,再找FF D9 */
if ((pbuf[i] == 0XFF) && (pbuf[i + 1] == 0XD9) && headok)
{
jpglen = i - jpgstart + 2;
break;
}
}
if (jpglen) /* 正常的jpeg資料 */
{
/* 模式0,或者嘗試打開失敗,則建立新檔案 */
res = f_open(f_jpg, (const TCHAR *)pname, FA_WRITE | FA_CREATE_NEW);
if (res == 0)
{
pbuf += jpgstart; /* 偏移到0XFF,0XD8處 */
res = f_write(f_jpg, pbuf, jpglen, &bwr);
if (bwr != jpglen)res = 0XFE;
}
f_close(f_jpg);
}
else
{
res = 0XFD;
}
g_jpeg_data_len = 0;
sw_ov5640_mode(); /* 切換為OV5640模式 */
ov5640_rgb565_mode(); /* RGB565模式 */
dcmi_dma_init((uint32_t)&LCD->LCD_RAM, 0, 1, DMA_MDATAALIGN_HALFWORD,
DMA_MINC_DISABLE); /* DCMI DMA配置,MCU屏,豎屏 */
myfree(SRAMIN, f_jpg);
return res;
}
該函數實作OV5640的JPEG圖像采集,并儲存圖像到SD卡,完成JPEG拍照。該函數首先設定OV5640工作在JPEG模式,然後,設定輸出分辨率為WXGA(1280*800)。然後,開始采集JPEG資料,将第二幀JPEG資料,保留下來,并寫入SD卡裡面,完成一次JPEG拍照。這裡,我們丢棄第一幀JPEG資料,是防止采集到的圖像資料不完整,導緻圖檔錯誤。
另外,在儲存jpeg圖檔的時候,我們将0XFF,0XD8和0XFF,0XD9之外的資料,進行了剔除,隻留下0XFF,0XD8~0XFF,0XD9之間的資料,保證圖檔檔案最小,且無其他亂的資料。
注意,在儲存圖檔的時候,必須将PC8/9/11切換為SD卡模式,并關閉OV5640的輸出。在圖檔儲存完成以後,切換回OV5640模式,并重新使能OV5640的輸出。
最後介紹的是main函數,其定義如下:
int main(void)
{
uint8_t res;
float fac;
uint8_t *pname; /* 帶路徑的檔案名 */
uint8_t key; /* 鍵值 */
uint8_t i;
uint8_t sd_ok = 1; /* 0,sd卡不正常;1,SD卡正常 */
uint8_t scale = 1; /* 預設是全尺寸縮放 */
uint8_t msgbuf[15]; /* 消息緩存區 */
uint16_t outputheight = 0;
sys_cache_enable(); /* 打開L1-Cache */
HAL_Init(); /* 初始化HAL庫 */
sys_stm32_clock_init(240, 2, 2, 4); /* 設定時鐘, 480Mhz */
delay_init(480); /* 延時初始化 */
usart_init(115200); /* 序列槽初始化為115200 */
usmart_dev.init(240); /* 初始化USMART */
mpu_memory_protection(); /* 保護相關存儲區域 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
key_init(); /* 初始化按鍵 */
beep_init(); /* 初始化蜂鳴器 */
ov5640_init(); /* 初始化OV5640 */
sw_sdcard_mode(); /* 首先切換為OV5640模式 */
piclib_init(); /* 初始化畫圖 */
my_mem_init(SRAMIN); /* 初始化内部記憶體池(AXI) */
my_mem_init(SRAM12); /* 初始化SRAM12記憶體池(SRAM1+SRAM2) */
my_mem_init(SRAM4); /* 初始化SRAM4記憶體池(SRAM4) */
my_mem_init(SRAMDTCM); /* 初始化DTCM記憶體池(DTCM) */
my_mem_init(SRAMITCM); /* 初始化ITCM記憶體池(ITCM) */
exfuns_init(); /* 為fatfs相關變量申請記憶體 */
f_mount(fs[0], "0:", 1); /* 挂載SD卡 */
f_mount(fs[1], "1:", 1); /* 挂載FLASH */
// lcd_display_dir(1); /* 設定成橫屏 */
while (fonts_init()) /* 檢查字庫 */
{
lcd_show_string(30, 50, 200, 16, 16, "Font Error!", RED);
delay_ms(200);
lcd_fill(30, 50, 240, 66, WHITE); /* 清除顯示 */
delay_ms(200);
}
text_show_string(30, 50, 200, 16, "正點原子STM32開發闆", 16, 0, RED);
text_show_string(30, 70, 200, 16, "硬體JPEG解碼 實驗", 16, 0, RED);
text_show_string(30, 90, 200, 16, "KEY0:拍照(bmp格式)", 16, 0, RED);
text_show_string(30, 110, 200, 16, "KEY1:拍照(jpg格式)", 16, 0, RED);
text_show_string(30, 130, 200, 16, "WK_UP:FullSize/Scale", 16, 0, RED);
res = f_mkdir("0:/PHOTO"); /* 建立PHOTO檔案夾 */
if (res != FR_EXIST && res != FR_OK) /* 發生了錯誤 */
{
res = f_mkdir("0:/PHOTO"); /* 建立PHOTO檔案夾 */
text_show_string(30, 150, 240, 16, "SD卡錯誤!", 16, 0, RED);
delay_ms(200);
text_show_string(30, 150, 240, 16, "拍照功能将不可用!", 16, 0, RED);
delay_ms(200);
sd_ok = 0;
}
/* 為jpeg dma接收申請記憶體 */
p_dcmi_line_buf[0] = mymalloc(SRAM12, jpeg_line_size * 4);
/* 為jpeg dma接收申請記憶體 */
p_dcmi_line_buf[1] = mymalloc(SRAM12, jpeg_line_size * 4);
p_jpeg_data_buf = mymalloc(SRAMIN, jpeg_buf_size); /* 為jpeg檔案申請記憶體 */
pname = mymalloc(SRAMIN, 30); /* 為帶路徑的檔案名配置設定30個位元組的記憶體 */
while (pname == NULL || !p_dcmi_line_buf[0] || !p_dcmi_line_buf[1]
|| !p_jpeg_data_buf) /* 記憶體配置設定出錯 */
{
text_show_string(30, 150, 240, 16, "記憶體配置設定失敗!", 16, 0, RED);
delay_ms(200);
lcd_fill(30, 150, 240, 146, WHITE); /* 清除顯示 */
delay_ms(200);
}
while (ov5640_init()) /* 初始化OV5640 */
{
text_show_string(30, 150, 240, 16, "OV5640 錯誤!", 16, 0, RED);
delay_ms(200);
lcd_fill(30, 150, 239, 206, WHITE);
delay_ms(200);
}
delay_ms(100);
text_show_string(30, 170, 230, 16, "OV5640 正常", 16, 0, RED);
/* 自動對焦初始化 */
ov5640_rgb565_mode(); /* RGB565模式 */
ov5640_focus_init();
ov5640_light_mode(0); /* 自動模式 */
ov5640_color_saturation(3); /* 色彩飽和度0 */
ov5640_brightness(4); /* 亮度0 */
ov5640_contrast(3); /* 對比度0 */
ov5640_sharpness(33); /* 自動銳度 */
ov5640_focus_constant(); /* 啟動持續對焦 */
dcmi_init(); /* DCMI配置 */
dcmi_dma_init((uint32_t)&LCD->LCD_RAM, 0, 1, DMA_MDATAALIGN_HALFWORD,
DMA_MINC_DISABLE); /* DCMI DMA配置,MCU屏,豎屏 */
if (lcddev.height >= 800)
{
g_yoffset = (lcddev.height - 800) / 2;
outputheight = 800;
ov5640_write_reg(0x3035, 0X51); /* 降低輸出幀率,否則可能抖動 */
}
else
{
g_yoffset = 0;
outputheight = lcddev.height;
}
g_curline = g_yoffset; /* 行數複位 */
ov5640_outsize_set(16, 4, lcddev.width, outputheight); /* 滿屏縮放顯示 */
dcmi_start(); /* 啟動傳輸 */
lcd_clear(BLACK);
while (1)
{
key = key_scan(0); /* 不支援連按 */
if (key)
{
/* 如果是BMP拍照,則等待1秒鐘,去抖動,以獲得穩定的bmp照片 */
if (key == KEY0_PRES)
{
delay_ms(300);
g_bmp_request = 1; /* 請求關閉DCMI */
while (g_bmp_request); /* 等帶請求處理完成 */
}
else
{
dcmi_stop();
}
if (key == WKUP_PRES) /* 縮放處理 */
{
scale = !scale;
if (scale == 0)
{
fac = (float)800 / outputheight; /* 得到比例因子 */
ov5640_outsize_set((1280 - fac * lcddev.width) / 2, (800
- fac * outputheight) / 2, lcddev.width, outputheight);
sprintf((char *)msgbuf, "Full Size 1:1");
}
else
{
ov5640_outsize_set(16, 4, lcddev.width, outputheight);
sprintf((char *)msgbuf, "Scale");
}
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, g_yoffset, lcddev.width,
outputheight, 0);
sw_ov5640_mode(); /* 切換為OV5640模式 */
}
else if (key == KEY1_PRES) /* JPG拍照 */
{
camera_new_pathname(pname, 1); /* 得到檔案名 */
res = ov5640_jpg_photo(pname);
if (scale == 0)
{
fac = (float)800 / outputheight; /* 得到比例因子 */
ov5640_outsize_set((1280 - fac * lcddev.width) / 2, (800
- fac * outputheight) / 2, lcddev.width, outputheight);
}
else
{
ov5640_outsize_set(16, 4, lcddev.width, outputheight);
}
/* 降低輸出幀率,否則可能抖動 */
if (lcddev.height >= 800)ov5640_write_reg(0x3035, 0X51);
}
if (res) /* 拍照有誤 */
{
text_show_string(30, 130, 240, 16, "寫入檔案錯誤!", 16, 0, RED);
}
else
{
text_show_string(30, 130, 240, 16, "拍照成功!", 16, 0, RED);
text_show_string(30, 150, 240, 16, "儲存為:", 16, 0, RED);
text_show_string(30 + 56, 150, 240, 16, (char*)pname,
16, 0, RED);
BEEP(1); /* 蜂鳴器短叫,提示拍照完成 */
delay_ms(100);
BEEP(0); /* 關閉蜂鳴器 */
}
delay_ms(1000); /* 等待1秒鐘 */
/* 這裡先使能dcmi,然後立即關閉DCMI,後面再開啟DCMI,可以防止RGB屏的側移問題 */
dcmi_start();
dcmi_stop();
}
else /* 提示SD卡錯誤 */
{
text_show_string(30, 130, 240, 16, "SD卡錯誤!", 16, 0, RED);
text_show_string(30, 150, 240, 16, "拍照功能不可用!", 16, 0, RED);
}
dcmi_start(); /* 開始顯示 */
}
delay_ms(10);
i++;
if (i == 20) /* DS0閃爍 */
{
i = 0;
LED0_TOGGLE();
}
}
}
該函數完成對各相關硬體的初始化,然後檢測OV5640,初始化OV5640位RGB565模式,顯示采集到的圖像到LCD上面,實作對圖像進行預覽。進入主循環以後,按KEY0按鍵,可以實作BMP拍照(實際上就是截屏,通過bmp_encode函數實作);按KEY1按鍵,可實作JPEG拍照(1280*800分辨率,通過ov5640_jpg_photo函數實作);按KEY_UP按鍵,可以實作圖像縮放/不縮放預覽。main函數實作了我們在50.2節所提到的功能。
至此照相機實驗代碼編寫完成。最後,本實驗可以通過USMART來設定OV5640的相關參數,将ov5640_contrast、ov5640_color_saturation和ov5640_light_mode等函數添加到USMART管理,即可通過序列槽設定OV5640的參數,友善調試。
50.4 下載下傳驗證
将程式下載下傳到開發闆後,可以看到LCD首先顯示一些實驗相關的資訊,如圖50.4.1所示:
圖50.4.1顯示實驗相關資訊
顯示了上圖的資訊後,自動進入監控界面。可以看到LED0不停的閃爍,提示程式已經在運作了。此外,LED1不停閃爍,提示進入DCMI中斷回調服務函數,進行jpeg資料處理。此時,我們可以按下KEY0和KEY1,即可進行bmp/jpg拍照。拍照得到的照片效果如圖50.4.2和圖50.4.3所示:
圖50.4.2 拍照樣圖(bmp拍照樣圖)
圖50.4.3 拍照樣圖(jpg拍照樣圖)
按KEY_UP可以實作縮放/不縮放顯示。最後,我們還可以通過USMART調用OV5640的相關控制函數,實作序列槽控制OV5640的線上參數修改,友善調試。