本项目实现了Funpack第11期的任务二,即使用LPC55S69开发板读取SD卡上的图片文件并显示在屏幕上。
图片使用的格式是最为常见的jpg图片,使用LPC55S69进行实时解码并显示。并实现了一个小型的文件列表功能,可以方便选取图片。
硬件说明
因为LPC55S69-EVK开发板在硬件设计上是兼容Arduino UNO的外设的,所以本次项目选用了一块儿为Arduino UNO 设计的1.8英寸TFT屏幕模块。这块屏幕模块使用SPI协议与MCU进行通讯。屏幕的分辨率是160x128,65K色(RGB565),控制器为ST7735S。
虽然模块上面带有SD卡槽,但是因为是和屏幕共享同一个SPI总线,使用不是很方便。我们还是使用开发板上自带的SD卡槽来读写SD卡。
实现思路
整个系统在官方SDK的SD卡Demo基础上修改而来。
官方Demo中已经实现了SD卡的检测、挂载和读取功能,我们还需要做的事情有
-
实现屏幕模块的驱动
-
读取SD卡上的文件,筛选出其中的JPG文件
-
实现最基础的文件列别UI绘制
-
实现对用户按键输入事件的检测
-
对JPG文件进行解码,并将解码后的图像传输到
首先是屏幕模块的驱动,这款屏幕模块因为是为Arduino设计,所以使用的是低速的SPI接口,因为速度不高,为了方便起见,我这里直接实现了一个软件SPI总线用来与屏幕模块进行通讯。相关API如下:
void SoftSPI_Init(SoftSPIDevice* spidev);
void SoftSPI_Transmit(SoftSPIDevice* spidev, uint8_t* data, uint32_t length);
void SoftSPI_Receive(SoftSPIDevice* spidev, uint8_t* data, uint32_t length);
void SoftSPI_TransmitReceive(SoftSPIDevice* spidev, uint8_t* txData, uint8_t* rxData, uint32_t length);
void SoftSPI_TransmitThenReceive(SoftSPIDevice* spidev, uint8_t* txData, uint16_t nbTx, uint8_t* rxData, uint16_t nbRx);
与屏幕模块的通讯问题解决后,只需要按照供应商提供的屏幕初始化代码,将初始化指令一一写入屏幕的寄存器中,我们就实现了对屏幕的驱动。在驱动中,我顺便添加了一些基本的绘图函数,比如填充屏幕、绘制字符串之类的函数。
void ST7735_Init(void);
void ST7735_DrawPixel(uint16_t x, uint16_t y, uint16_t color);
void ST7735_DrawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color);
void ST7735_DrawCircle(uint16_t x0, uint16_t y0, uint16_t r, uint16_t color);
void ST7735_FillCircle(uint16_t x0, uint16_t y0, uint16_t r, uint16_t color);
void ST7735_DrawTriangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1,uint16_t x2, uint16_t y2, uint16_t color);
void ST7735_FillTriangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color);
void ST7735_WriteString(uint16_t x, uint16_t y, const char* str, FontDef* font, uint16_t color, uint16_t bgcolor);
void ST7735_FillRectangle(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color);
void ST7735_FillScreen(uint16_t color);
void ST7735_DrawImage(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t* data);
void ST7735_InvertColors(bool invert);
在代码的开始部分,将屏幕全屏填充为蓝色后再填充为黑色,可以测试屏幕的初始化是否正常,填充函数是否工作,并且为后面的UI绘制做准备。
SoftSPI_Init(&lcdSpiPort);
SoftSPI_Transmit(&lcdSpiPort, work, 10);
ST7735_Init();
ST7735_FillScreen(ST7735_BLUE);
ST7735_FillScreen(ST7735_BLACK);
ST7735_WriteString(0, 0, "Initializing...", &FONT_SONG_6X12, ST7735_RED, ST7735_BLACK);
接下来对SD卡进行检测,如果发现SD卡正常连接,则对SD卡进行初始化,然后挂载文件系统即可。这部分代码很简单
if (sdcardWaitCardInsert() != kStatus_Success)
{
return -1;
}
if (f_mount(&g_fileSystem, driverNumberBuffer, 0U))
{
PRINTF("Mount volume failed.\r\n");
ST7735_WriteString(0, 0, "Card init err.", &FONT_SONG_6X12, ST7735_RED, ST7735_BLACK);
return -1;
}
如果成功挂载SD卡,下一步就是读取SD卡上的JPG图片文件列表并存储起来。为了方便,我使用了一个固定的数组来存储文件列表,设定了最大16个文件的上限。同时,为了节约资源,这里仅读取SD卡根目录下的文件。
在列出根目录文件的时候,需要顺带对文件名进行判断,如果是jpg文件则添加到文件列表,反之则不进行任何操作。
static void ListRootJpegFiles(){
FRESULT error;
DIR directory; /* Directory object */
FILINFO fileInformation;
if (f_opendir(&directory, "/"))
{
PRINTF("Open directory failed.\r\n");
return;
}
fileCounts = 0;
while (fileCounts <= 16)
{
error = f_readdir(&directory, &fileInformation);
/* To the end. */
if ((error != FR_OK) || (fileInformation.fname[0U] == 0U))
{
break;
}
if (fileInformation.fname[0] == '.')
{
continue;
}
if (fileInformation.fattrib & AM_DIR)
{
PRINTF("Directory file : %s.\r\n", fileInformation.fname);
continue;
}
else if(isStrEndsWithIgnoreCase(fileInformation.fname, "jpg") || isStrEndsWithIgnoreCase(fileInformation.fname, "jpeg"))
{
if(strlen(fileInformation.fname) >= 24){
continue;
}
PRINTF("fount file : %s\r\n", fileInformation.fname);
memcpy(fileList[fileCounts], fileInformation.fname, 24);
fileCounts++;
}
}
f_closedir(&directory);
}
有了文件列表后,我通过基本的文本和图形绘制,实现了一个小型的文件列表UI,把刚才获取的文件列表显示在了屏幕上。代码非常简单,仅有换页和切换选择两个函数。
static void ChangePage(uint32_t newPage){
ST7735_FillScreen(ST7735_BLACK);
int totalPages = fileCounts / itemsPerPage;
if(fileCounts % itemsPerPage != 0){
totalPages++;
}
if(newPage >= totalPages){
newPage = 0;
}
int startIndex = newPage * itemsPerPage;
int endIndex = (startIndex + 8) > fileCounts ? fileCounts : startIndex + 8;
uint16_t x = 16;
uint16_t y = 0;
for(int i = startIndex; i < endIndex; i++){
ST7735_WriteString(x, y, fileList[i], &FONT_SONG_6X12, ST7735_WHITE, ST7735_BLACK);
y+=16;
}
pageIndex = newPage;
}
static void ChageSelectedIndex(int32_t index){
if(index == selectedFileIndex || index >= fileCounts){
return;
}
uint16_t x = 8;
uint16_t y = 0;
if(index >= 0){
if(index / 8 != pageIndex){
ChangePage(index / 8);
}
else if(selectedFileIndex >= 0){
y = (selectedFileIndex % 8) * 16 + 6;
ST7735_FillCircle(x, y, 4, ST7735_BLACK);
}
selectedFileIndex = index;
}
else{
ChangePage(0);
selectedFileIndex = -1;
}
if(selectedFileIndex != -1){
y = (selectedFileIndex % 8) * 16 + 6;
ST7735_FillCircle(x, y, 4, ST7735_WHITE);
}
}
最后,程序进入循环,等待用户按键事件的输入。这里通过Xpresso的图形化配置功能,将对应按键的GPIO配置为输入,并且通过SDK提供的输入读取函数不停检测IO口的状态,如果发现是WKUP按键,则功能为切换选择的图片文件。
如果用户按下了USER按键,则读取当前选中的图片文件,然后进行解码。
while (true)
{
// Switch to next item
if(GPIO_PinRead(BOARD_INITPINS_WKUP_GPIO, BOARD_INITPINS_WKUP_PORT, BOARD_INITPINS_WKUP_PIN) == 0){
while(GPIO_PinRead(BOARD_INITPINS_WKUP_GPIO, BOARD_INITPINS_WKUP_PORT, BOARD_INITPINS_WKUP_PIN) == 0);
if(selectedFileIndex >= (fileCounts - 1)){
ChageSelectedIndex(0);
}
else{
ChageSelectedIndex(selectedFileIndex + 1);
}
}
// open file
else if(GPIO_PinRead(BOARD_INITPINS_KEY_GPIO, BOARD_INITPINS_KEY_PORT, BOARD_INITPINS_KEY_PIN) == 0){
while(GPIO_PinRead(BOARD_INITPINS_KEY_GPIO, BOARD_INITPINS_KEY_PORT, BOARD_INITPINS_KEY_PIN) == 0);
while(1){
// decode selected file
decodeJpedFile();
// waitting for key press
while(GPIO_PinRead(BOARD_INITPINS_KEY_GPIO, BOARD_INITPINS_KEY_PORT, BOARD_INITPINS_KEY_PIN)
&& GPIO_PinRead(BOARD_INITPINS_WKUP_GPIO, BOARD_INITPINS_WKUP_PORT, BOARD_INITPINS_WKUP_PIN));
if(GPIO_PinRead(BOARD_INITPINS_KEY_GPIO, BOARD_INITPINS_KEY_PORT, BOARD_INITPINS_KEY_PIN) == 0){
while(GPIO_PinRead(BOARD_INITPINS_KEY_GPIO, BOARD_INITPINS_KEY_PORT, BOARD_INITPINS_KEY_PIN) == 0);
int index = selectedFileIndex;
selectedFileIndex = -1;
ChangePage(selectedFileIndex/8);
ChageSelectedIndex(index);
break;
}
else{
selectedFileIndex += 1;
if(selectedFileIndex >= fileCounts){
selectedFileIndex = 0;
}
ST7735_FillScreen(ST7735_BLACK);
}
}
}
SysTick_DelayTicks(100);
}
因为LPC55S69是没有硬件JPEG解码器的,所以这里使用的是一个开源的tjpgd库,这个JPEG解码库非常小巧且功能齐全,使用上也非常简单。只需要我们提供一个输入、一个输出回调函数,即可完成解码。在预解码阶段,我们可以获取图片的分辨率,并且修改对应的缩放比例。另外需要注意的是,解码输出的数据是小字节序的,而SPI传输则是高字节优先,所以我们还需要进行一下字节序转换
size_t in_func (JDEC* jd, uint8_t* buff, size_t nbyte)
{
if (buff) {
UINT read = 0;
FRESULT rst = f_read(&decodingFile, buff, nbyte, (UINT*)&read);
if(rst != FR_OK){
return 0;
}
return read;
}
f_lseek(&decodingFile, decodingFile.fptr + nbyte);
return nbyte;
}
uint16_t baseX = 0, baseY = 16;
int out_func (JDEC* jd, void* bitmap, JRECT* rect)
{
uint8_t* pBuffer = bitmap;
int size = (rect->right - rect->left + 1)*(rect->bottom - rect->top + 1);
for(int i = 0; i < size; i++){
uint8_t dat = pBuffer[2*i];
pBuffer[2*i] = pBuffer[2*i + 1];
pBuffer[2*i + 1] = dat;
}
ST7735_DrawImage(
baseX + rect->left,
baseY + rect->top,
rect->right - rect->left + 1,
rect->bottom - rect->top + 1,
bitmap);
return 1; /* 继续解码 */
}
static void decodeJpedFile() {
if (selectedFileIndex < 0) {
return;
}
FRESULT rst = f_open(&decodingFile, fileList[selectedFileIndex], FA_OPEN_EXISTING | FA_READ);
if (rst != FR_OK) {
f_close(&decodingFile);
return;
}
ST7735_FillScreen(ST7735_BLACK);
JRESULT jrst = jd_prepare(&jdec, in_func, buffer, 3100, NULL);
if (jrst == JDR_OK) {
int scale = 0;
if (jdec.width > 1280 || jdec.height > 1024) {
scale = -1;
} else if (jdec.width > 640 || jdec.height > 512) {
scale = 3;
} else if (jdec.width > 320 || jdec.height > 256) {
scale = 2;
} else if (jdec.width > 160 || jdec.height > 128) {
scale = 1;
}
if (scale >= 0) {
int width = jdec.width / pow(2, scale);
int height = jdec.height / pow(2, scale);
baseX = (160 - width) / 2;
baseY = (128 - height) / 2;
jd_decomp(&jdec, out_func, scale);
ST7735_WriteString(0, 0, fileList[selectedFileIndex], &FONT_SONG_6X12, ST7735_WHITE, ST7735_BLACK);
} else {
ST7735_WriteString(0, 0, "Image is too big!", &FONT_SONG_6X12, ST7735_WHITE, ST7735_BLACK);
}
} else {
ST7735_WriteString(0, 0, "File not support!", &FONT_SONG_6X12, ST7735_WHITE, ST7735_BLACK);
SysTick_DelayTicks(2000);
}
f_close(&decodingFile);
}
最终效果
显示文件列表
解码图片并显示
结语
Funpack活动为我们这些喜欢折腾各类技术的电子爱好者提供了一个非常好的平台,希望这个活动能长期做下去,并且越办越好。