任务介绍
本项目实现了Funpack第11期活动的任务二,读取SD卡中预先存入的图像,显示在OLED屏幕上。
硬件平台
本期活动的主角:恩智浦推出的LPC55S69开发板。它的主控芯片使用的是双核基于Cortex-M33的微控制器,在设计中能够灵活地平衡性能和功耗,这块开发板搭载了丰富的板载资源,预留了足够多的IO接口,并且配备了SD卡槽、加速度计和音频编解码器,我们可以用它实现各种复杂的功能。
任务分析与实现
对任务二做一个简单的分解,可以分成三个部分:SD卡读取、图像处理、OLED显示,我们依次实现它们。
首先是SD卡读取,这一部分实现比较简单,在官方SDK和RT-Thread的BSP中都做好了适配。我选用的是RT-Thread实现,需要在RT-Thread的配置中启用FAT文件系统的支持,重新编译,在工程中先将SD卡设备挂载文件系统,然后就可以通过标准的文件API读写数据。
第二步是图像处理,因为OLED可以显示的图像数据和原始图像数据是不一样的,需要我们进行预处理,这里可以先在电脑上进行处理,将预处理后的图像文件放到SD卡上,这样可以简化单片机上的程序逻辑。我写了一个图像处理的Python程序,可以将任意格式和大小的原始图像,处理并转化为可以直接写入OLED显存的数据格式,并保存为一个文件。
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
from PIL import ImageGrab, Image
############################### Global Variables ###############################
oledWidth = 128 # Number of pixel columns handled by the OLED screen
oledHeight = 64 # Number of pixel rows managed by the OLED screen
threshold = 127 # Below this contrast value, pixel is considered as activated
def getImage():
# Check number of arguments
if len(sys.argv) == 3:
# Try to open the image
try:
im = Image.open(sys.argv[1])
# im = ImageGrab.grab()
im = im.convert(mode="L")
except:
print("Error: unable to grab", sys.argv[1], file=sys.stderr)
exit(-1)
# Check image dimensions
width,height = im.size
if (width != oledWidth or height != oledHeight):
im = im.resize((oledWidth, oledHeight), Image.ANTIALIAS)
return im
else :
print("Error: invalid number of arguments", file=sys.stderr)
print("Usage:")
print("python " + sys.argv[0] + " <input-filename> <output-bin-filename>")
exit(-1)
def grabDesktop():
im = ImageGrab.grab()
im = im.convert(mode="L")
im = im.resize((oledWidth, oledHeight), Image.ANTIALIAS)
return im
def convert(pixels) :
data = [[0 for x in range(oledHeight//8)] for x in range(oledWidth)]
for i in range(oledWidth):
for j in range(oledHeight//8):
for bit in range(8):
data[i][j] |= (pixels[i][j*8 + bit] << bit)
return data
def toBinary(im):
# Convert image to monochrome if necessary
if (im.mode != "1"):
im.convert("1")
# Allocate array to hold binary values
binary = [[0 for x in range(oledHeight)] for x in range(oledWidth)]
# Convert to binary values by using threshold
for j in range(oledHeight):
for i in range(oledWidth):
value = im.getpixel((i, j))
# print(value)
# Set bit if the pixel contrast is below the threshold value
binary[i][j] = int(value < threshold)
return binary
def output_file(data):
buffer = b''
for j in range(oledHeight//8):
for i in range(oledWidth):
buffer += data[i][j].to_bytes(1, 'little')
# print(buffer)
header_file = open(sys.argv[2], "wb")
header_file.write(buffer)
header_file.close()
return buffer
def output(data):
buffer = b''
for j in range(oledHeight//8):
for i in range(oledWidth):
buffer += data[i][j].to_bytes(1, 'little')
return buffer
def getFrame():
image = grabDesktop()
binary = toBinary(image)
data = convert(binary)
return output(data)
#################################### Main ######################################
if __name__ == '__main__':
image = getImage()
binary = toBinary(image)
data = convert(binary)
output_file(data)
# print(output_file(data))
第三步是OLED显示,这一步我们同样可以使用RT-Thread提供好的SSD1306程序包,只需要在menuconfig中打开相应的配置。
经过以上三步准备,我们编写一个60行代码的程序,读取SD卡中的数据,并写入OLED的显存,任务二就可以完成了。
static void SD_Picture(void* arg)
{
rt_thread_mdelay(100);
//mkfs("elm","sd0");
if(dfs_mount("sdcard0","/","elm",0,0)==0)
{
rt_kprintf("dfs mount success\n");
}
else
{
rt_kprintf("dfs mount failed\n");
return;
}
uint8_t rbuf[1024] = {0};
int rsize = 0;
int fd = 0;
fd = open("/mybin.bin.\n", O_RDONLY);
if(fd>0)
{
rsize = read(fd, rbuf, 1024);
close(fd);
if(rsize>0)
{
rt_kprintf("READ (%d) byte.\n",rsize);
ssd1306_Init();
ssd1306_Fill(Black);
ssd1306_FillBuffer(rbuf, sizeof(rbuf));
ssd1306_UpdateScreen();
}
}
else
{
rt_kprintf("can not open file.\n");
}
}
效果展示
任务扩展-基于LPC55S69的实时OLED电脑显示器
既然现在我们已经可以将任意图像显示到OLED上,能不能再进一步,把OLED变成一个实时的超低画质电脑显示器呢?这完全是可行的,但是为了保证数据流的实时传输,就不能使用SD卡读写的方法了,我们可以尝试基于串口、USB,甚至是网络来实现传输。
由于手上正好有一块ESP8266模块,这里我决定使用网络来进行传输,首先将ESP8266模块写入标准的AT固件,这一点官网有教程,不再赘述。随后我们打开RT-Thread的网络以及AT设备支持,现在我们的开发板就可以支持网络通信了。
static uint32_t PlayStream(void)
{
uint32_t frameId = 0;
struct sockaddr_in client_addr;
struct sockaddr_in server_addr;
struct netdev *netdev = RT_NULL;
int sockfd = -1;
netdev = netdev_get_by_name("esp0");
if (netdev == RT_NULL)
{
rt_kprintf("get network interface esp0 failed.\n");
return -RT_ERROR;
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
rt_kprintf("Socket create failed.\n");
return -RT_ERROR;
}
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_HOST);
rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0)
{
rt_kprintf("socket connect failed!\n");
closesocket(sockfd);
return -RT_ERROR;
}
else
{
rt_kprintf("connect to server %s success!\r\n", g_serverIp);
}
frameId = 0;
while (1) {
uint32_t start = rt_tick_get();
uint32_t request = htonl(frameId); // to big endian
ssize_t retval = send(sockfd, &request, sizeof(request), 0);
if (retval < 0) {
break;
}
uint32_t status = 0;
retval = recv(sockfd, &status, sizeof(status), 0);
if (retval != sizeof(status)) {
printf("lwip_recv status for frame %d failed or done, %d!\r\n", frameId, retval);
break;
}
status = ntohl(status);
if (status != STATUS_OK) {
break;
}
ssize_t bodyLen = 0;
retval = recv(sockfd, &bodyLen, sizeof(bodyLen), 0);
if (retval != sizeof(bodyLen)) {
printf("lwip_recv bodyLen for frame %d failed or done, %d!\r\n", frameId, retval);
break;
}
bodyLen = ntohl(bodyLen);
ssize_t bodyReceived = 0;
while (bodyReceived < bodyLen) {
retval = recv(sockfd, &g_streamBuffer[bodyReceived], bodyLen, 0);
bodyReceived += retval;
}
ssd1306_FillBuffer(g_streamBuffer, sizeof(g_streamBuffer));
ssd1306_UpdateScreen();
frameId++;
uint32_t end = rt_tick_get();
printf("FPS: %.2f\r\n", RT_TICK_PER_SECOND / (float)(end - start));
}
printf("playing video done, played frames: %d!\r\n", frameId);
do_close:
closesocket(sockfd);
return frameId;
}
随后,我们可以简单改写任务二的程序,把单片机中文件读写的逻辑改为TCP通信,把电脑端读取任意图像文件的程序改为抓取桌面图片,并启动一个TCP服务器,只要收到来自单片机的图片请求,就不断地通过TCP网络发送图像数据。经过这样一波魔改,开发板就成功地变成了一个电脑显示器,实际的效果可以在视频中看到。
活动感想
很荣幸参加本期的Funpack活动,这也是我第三次参加Funpack系列的活动。通过这一次活动,我第一次正式地接触并使用LPC系列的单片机,在以往的印象里,NXP一直专注于汽车电子,门槛高,只有非常专业的领域大佬才能玩转。但这次我欣喜于LPC系列的配置工具和SDK支持的完善程度,甚至在RT-Thread中也对本次任务的LPC55S69-EVK开发板做了相对完善的板级支持,这对我们开发人员和爱好者来说是非常友好的。希望NXP能通过类似的活动加强宣传和推广,也进一步完善自己的开发工具和软件生态。
最后,感谢硬禾学堂和得捷电子联合举办的Funpack活动,祝硬禾的活动越办越好!