项目描述及需要实现的功能
本次活动给出的可选任务如下:
任务一:
读取SD卡(SD卡自备)中的音频文件,使用板卡上的3.5mm音频接口播放音乐。
任务二:
读取SD卡中预先存入的图像,显示在屏幕上(OLED或LCD)。
任务三:
实现USB-UART Hub,自己设计通信方式,实现USB到多路UART的收发。
任务四:
移植Micropython,并实现串口通信和控制IO口。
本人对任务二进行了实现,使用LPCX55S69开发板配合使用IIC协议、的OLED屏幕,在显示图像的基础上完成了BadApple动画播放功能。
设计思路
本项目的设计思路总体比较简单,由以下两个功能组成:
-
SD卡文件读写
-
OLED屏幕刷新
本次活动采用的LPC55S69芯片性能非常强大,NXP公司官方的SDK中提供了较多示例代码,其中就包括SD卡读写的相关示例代码。在实现屏幕刷新功能时,主要使用U8g2库完成。
项目解析图片数据生成
项目使用的屏幕尺寸是128*64,为了能够正常显示,可以将图片提前处理成易于显示的格式。当获取视频文件后,使用ffmpeg工具将视频文件输出为图片,参考命令如下:
ffmpeg -i badapple.mp4 -r 30 -qscale:v 2 source/%07d.jpg
后续编写Python脚本,对每张图片进行处理,并将所有图片存储到一个DAT文件当中。对于一张图片的处理流程如下:
-
使用PIL库读入图片文件,并将尺寸修改为128*64
-
将图片转化为二值图
-
将图片数据转化为一维数组,八位一组以字节形式写入到文件
from PIL import Image
import numpy as np
import os
def img2bmp(file, source_folder, aim_folder):
im = Image.open(source_folder +
'/' + file)
new_im_size = np.array([128, 64]).astype(int)
im = im.resize(new_im_size)
im = im.convert("L")
threshold = 200 # 阈值
table = []
for i in range(256):
if i < threshold:
table.append(0)
else:
table.append(1)
# 转化为二值图
im = im.point(table, "1")
im.save("test.png")
# 写入二进制文件
im_np = np.array(im)
im_np = im_np.flatten()
# print(im_np)
# 已知共有128 64个像素点,每八位写入一次 共写入128*8=1024次
fh = open('ba15.DAT', 'ab')
for i in range(1024):
start = i * 8
end = start + 8
cur_byte = ""
for k in range(start, end):
if im_np[k]:
cur_byte += '1'
else:
cur_byte += '0'
# print(cur_byte)
cur_byte = cur_byte[::-1] # u8g2按小端方式渲染 需要对字符串反转后再处理
if cur_byte == '':
print(1)
continue
fh.write(int(cur_byte, 2).to_bytes(1, 'big'))
source_folder = "source_15"
aim_folder = "out"
source_file = [f for f in os.listdir(source_folder)]
count = 0
for f in source_file:
img2bmp(f, source_folder, aim_folder)
count += 1
print(count)
print("finish!")
SD卡读取及图片绘制
因为任务较为简单,加上对较为复杂的MCU开发经验不足,代码主要在官方SD卡示例上修改得到,同时参考了U8g2的应用笔记,主体如下:
/*
* Copyright (c) 2015, Freescale Semiconductor, Inc.
* Copyright 2016-2017 NXP
* All rights reserved.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#include <stdio.h>
#include <string.h>
#include "fsl_sd.h"
#include "fsl_debug_console.h"
#include "ff.h"
#include "diskio.h"
#include "fsl_sd_disk.h"
#include "pin_mux.h"
#include "board.h"
#include "sdmmc_config.h"
#include <stdbool.h>
#include "fsl_iocon.h"
#include "fsl_power.h"
#include "driver_oled_ssd1306.h"
#include "u8g2.h"
/*******************************************************************************
* Definitions
******************************************************************************/
/* buffer size (in byte) for read/write operations */
#define BUFFER_SIZE (1024U)
/*******************************************************************************
* Prototypes
******************************************************************************/
/*!
* @brief wait card insert function.
*/
static status_t sdcardWaitCardInsert(void);
/*******************************************************************************
* Variables
******************************************************************************/
static FATFS g_fileSystem; /* File system object */
static FIL g_fileObject; /* File object */
u8g2_t u8g2;
/*! @brief Data read from the card */
SDK_ALIGN(uint8_t g_bufferRead[BUFFER_SIZE],
BOARD_SDMMC_DATA_BUFFER_ALIGN_SIZE);
/*******************************************************************************
* Code
******************************************************************************/
/*!
* @brief Main function
*/
int main(void) {
FRESULT error;
UINT bytesRead;
const TCHAR driverNumberBuffer[3U] = {SDDISK + '0', ':', '/'};
BYTE work[FF_MAX_SS];
/* set BOD VBAT level to 1.65V */
POWER_SetBodVbatLevel(kPOWER_BodVbatLevel1650mv, kPOWER_BodHystLevel50mv,
false);
CLOCK_EnableClock(kCLOCK_InputMux);
/* attach 12 MHz clock to FLEXCOMM0 (debug console) */
CLOCK_AttachClk(BOARD_DEBUG_UART_CLK_ATTACH);
BOARD_InitPins();
BOARD_BootClockPLL150M();
BOARD_InitDebugConsole();
CLOCK_EnableClock(kCLOCK_Gpio0);
CLOCK_EnableClock(kCLOCK_Gpio1);
/* setup display by different mode */
#ifdef SSD1306_USE_I2C_GPIO
u8g2_Setup_ssd1306_i2c_128x64_noname_2(&u8g2, U8G2_R0, u8x8_byte_sw_i2c,
u8x8_gpio_and_delay_lpc55);
#endif
#ifdef SSD1306_USE_I2C_HW
u8g2_Setup_ssd1306_i2c_128x64_noname_f(
&u8g2, U8G2_R0, u8x8_byte_hw_i2c_lpc55, u8x8_gpio_and_delay_lpc55);
#endif
#ifdef SSD1306_USE_SPI_GPIO
u8g2_Setup_ssd1306_128x64_noname_2(&u8g2, U8G2_R0, u8x8_byte_4wire_sw_spi,
u8x8_gpio_and_delay_lpc55);
#endif
#ifdef SSD1306_USE_SPI_HW
u8g2_Setup_ssd1306_128x64_noname_2(&u8g2, U8G2_R0,
u8x8_byte_4wire_hw_spi_lpc55,
u8x8_gpio_and_delay_lpc55);
#endif
u8g2_InitDisplay(&u8g2);
u8g2_SetPowerSave(&u8g2, 0);
if (sdcardWaitCardInsert() != kStatus_Success) {
return -1;
}
if (f_mount(&g_fileSystem, driverNumberBuffer, 0U)) {
PRINTF("Mount volume failed.\r\n");
return -1;
}
#if (FF_FS_RPATH >= 2U)
error = f_chdrive((char const *)&driverNumberBuffer[0U]);
if (error)
{
PRINTF("Change drive failed.\r\n");
return -1;
}
#endif
error = f_open(&g_fileObject, _T("BA15.dat"), (FA_READ));
if (error) {
PRINTF("%d", error);
PRINTF("Open file failed.\r\n");
return -1;
}
while (1) {
// 打开文件
error = f_read(&g_fileObject, g_bufferRead, sizeof(g_bufferRead),
&bytesRead);
if ((error) || (bytesRead != sizeof(g_bufferRead))) {
PRINTF("Read file failed. \r\n");
PRINTF("%d", error);
return -1;
}
u8g2_ClearBuffer(&u8g2);
u8g2_DrawXBMP(&u8g2, 0, 0, 128, 64, g_bufferRead);
u8g2_SendBuffer(&u8g2);
}
if (f_close(&g_fileObject)) {
PRINTF("\r\nClose file failed.\r\n");
return -1;
}
while (true) {
}
}
static status_t sdcardWaitCardInsert(void) {
BOARD_SD_Config(&g_sd, NULL, BOARD_SDMMC_SD_HOST_IRQ_PRIORITY, NULL);
/* SD host init function */
if (SD_HostInit(&g_sd) != kStatus_Success) {
PRINTF("\r\nSD host init fail\r\n");
return kStatus_Fail;
}
/* wait card insert */
if (SD_PollingCardInsert(&g_sd, kSD_Inserted) == kStatus_Success) {
PRINTF("\r\nCard inserted.\r\n");
/* power off card */
SD_SetCardPower(&g_sd, false);
/* power on the card */
SD_SetCardPower(&g_sd, true);
} else {
PRINTF("\r\nCard detect fail.\r\n");
return kStatus_Fail;
}
return kStatus_Success;
}
代码总体比较简单易懂,首先打开SD卡中的数据文件,接着使用f_read函数进行读取,并刷新屏幕。这里将输入缓冲区的尺寸设置为1024,读取一次得到的数据刚好用于刷新整个屏幕,同时不需要考虑额外的终止条件,只需要一直读取一直刷新直到文件末尾即可。
U8g2库在使用时需要指定对应的构造器,应用笔记中给出的示例代码对当前使用的协议进行了宏定义,只需要修改宏定义的值便能正确初始化,如果没有深度使用需要不去仔细研究U8g2构造原理也能正常刷新屏幕。
总结与心得体会
首先感谢Funpack活动提供了让我接触LPC55S69芯片的机会,在感叹官方SDK代码的丰富和工整的同时我也认识到了自己在MCU开发领域技能上的欠缺。平心而论,本项目实现的功能并不复杂,但对于只接触过arduino的我来说,IDE的使用确实是一个比较大的挑战。项目代码的构成方式、三方库导入方法、管脚分配,在不断地尝试和查找资料的过程中,我着实积累了一些开发经验。
参考:
如何移植U8g2 到LPC55(S)6x 并驱动单色OLED 屏幕 (nxp.com)
MSDFATFSAPIRM, Freescale MSD FATFS API - Reference Manual (nxp.com.cn)