badapple 是什么,上网随便查了下,没看出个究竟,不过有个关于这个挺火的标签或者主题 《 有屏幕的地方就有 badapple 》,网上有很多人用很多方式播放 badapple 动画,有用单片机在 OLED、LCD、LED点阵播放,有在 PC 上用各种编程语言、以各种形式播放,还有在示波器上播放。我之前也玩过

我用到的 badapple 数据是从这里 OLED-STM32 找到的,在这里,badapple 动画数据被做成了一个二进制文件,因为是在一个 128 X64 的OLED上播放,badapple 数据被做成了一帧是128 X 64 个点的单色位图形式,可以直接写入 OLED上显示,看了下这个文件:

有 5.13 M byte,一帧是 128 X 64 / 8 = 1024 字节的话,就有 5383168 /1024 = 5257 帧了。

用单片机来播放,单片机内部 Flash 一般都比较小,无法把 badapple 数据放入单片机自带的 Flash 中,所以用单片机播放的话,大概就 2 种思路:

  • 把 badapple 放到外部存储,如 SPI Flash、SD 卡等,单片机从这些存储设备读出数据,然后显示到显示器上,
  • PC 或者 手机 等把 badapple 数据通过通讯接口传送给 MCU,通讯接口可以是UART、USB、网络、蓝牙等,单片机从这些通讯接口接收到数据然后显示到显示器上

这里尝试把 badapple 数据编译进 nuc980 固件中,这样的话,编译出来的固件至少 5.13 M字节,对于只能用内部flash存储固件的单片机来说的话,应该是不行的,我还没见到过内部flash有这么大的单片机。对于 NUC980 可以用外部flash、有这么大的 DRAM,是完全可以实现的,就像之前说的,为所欲为了,如果 NUC980 跑 Linux 的话,这5点几兆算小了。

这里要做的有:

  • 先把显示器驱动起来,
  • 解决怎么把 badapple 数据编译进固件

硬件

我这里用到的显示器是 SPI 接口的 OLED 模块,驱动芯片是 SSD1306,如下:

既然是使用 SPI接口,就要用到 NUC980 的SPI接口了,上图,还是这样用过了好几次的图,NuMaker-RTU-NUC980 板子引出的 IO:

可以看到板子上引出了 SPI0,还需要 2 个 GPIO 用于 OELD的 RST、DC,接线如下:

OLED     NUC980
D0 <-- PC6
D1 <-- PC8
RST <-- PB4
DC <-- PB6
CS <-- PC5

实物如下:

把 OLED 驱动起来

RT-Thread 中 SPI 接口也有相应的驱动框架、对于的API,具体可以查看 RT-Thread 相应文档 -- SPI设备

首先定义一个 spi 设备,然后挂载到 SPI0,设置 SPI 参数,并把另外用到的 2 个 IO 设置为输出,如下:

static struct rt_spi_device spi_dev_lcd;

#define SSD1306_DC_PIN  NU_GET_PININDEX(NU_PB, 6)
#define SSD1306_RES_PIN NU_GET_PININDEX(NU_PB, 4) static int rt_hw_ssd1306_config(void)
{
if (rt_spi_bus_attach_device(&spi_dev_lcd, "lcd_ssd1306", "spi0", RT_NULL) != RT_EOK)
return -RT_ERROR; /* config spi */
{
struct rt_spi_configuration cfg;
cfg.data_width = 8;
cfg.mode = RT_SPI_MASTER | RT_SPI_MODE_3 | RT_SPI_MSB;
cfg.max_hz = 42 * 1000 * 1000; /* 42M,SPI max 42MHz,lcd 4-wire spi */ rt_spi_configure(&spi_dev_lcd, &cfg);
} rt_pin_mode(SSD1306_DC_PIN, PIN_MODE_OUTPUT);
rt_pin_mode(SSD1306_RES_PIN, PIN_MODE_OUTPUT); return RT_EOK;
}

然后实现对 OLED 写命令、数据函数:

static rt_err_t ssd1306_write_cmd(const rt_uint8_t cmd)
{
rt_size_t len; rt_pin_write(SSD1306_DC_PIN, PIN_LOW); rt_spi_transfer(&spi_dev_lcd, (const void *)&cmd, NULL, 1);
if (len != 1)
{
LOG_I("ssd1306_write_cmd error. %d", len);
return -RT_ERROR;
}
else
{
return RT_EOK;
}
} static rt_err_t ssd1306_write_data(const rt_uint8_t data)
{
rt_size_t len; rt_pin_write(SSD1306_DC_PIN, PIN_HIGH);
rt_spi_transfer(&spi_dev_lcd, (const void *)&data, NULL, 1); if (len != 1)
{
LOG_I("ssd1306_write_data error. %d", len);
return -RT_ERROR;
}
else
{
return RT_EOK;
}
}

实现 OLED 初始化、写屏、清屏函数:

void ssd1306_fill(uint8_t *dat)
{
uint8_t i,n;
for(i=0;i<8;i++)
{
ssd1306_write_cmd (0xb0+i);
ssd1306_write_cmd (0x00);
ssd1306_write_cmd (0x10);
for(n=0;n<128;n++)
{
ssd1306_write_data(*dat);
dat ++;
} }
} void ssd1306_clear(void)
{
uint8_t i,n;
for(i=0;i<8;i++)
{
ssd1306_write_cmd (0xb0+i);
ssd1306_write_cmd (0x00);
ssd1306_write_cmd (0x10);
for(n=0;n<128;n++)
{
ssd1306_write_data(0x00);
dat ++;
} }
} static int rt_hw_ssd1306_init(void)
{ rt_hw_ssd1306_config(); rt_pin_write(SSD1306_RES_PIN, PIN_HIGH);
rt_thread_delay(RT_TICK_PER_SECOND / 10);
rt_pin_write(SSD1306_RES_PIN, PIN_LOW);
//wait at least 100ms for reset
rt_thread_delay(RT_TICK_PER_SECOND / 10);
rt_pin_write(SSD1306_RES_PIN, PIN_HIGH); ssd1306_write_cmd(0xAE);
ssd1306_write_cmd(0xD5);
ssd1306_write_cmd(80);
ssd1306_write_cmd(0xA8);
ssd1306_write_cmd(0X3F);
ssd1306_write_cmd(0xD3);
ssd1306_write_cmd(0X00); ssd1306_write_cmd(0x40); ssd1306_write_cmd(0x8D);
ssd1306_write_cmd(0x14);
ssd1306_write_cmd(0x20);
ssd1306_write_cmd(0x02);
ssd1306_write_cmd(0xA1);
ssd1306_write_cmd(0xC0);
ssd1306_write_cmd(0xDA);
ssd1306_write_cmd(0x12); ssd1306_write_cmd(0x81);
ssd1306_write_cmd(0xEF);
ssd1306_write_cmd(0xD9);
ssd1306_write_cmd(0xf1);
ssd1306_write_cmd(0xDB);
ssd1306_write_cmd(0x30); ssd1306_write_cmd(0xA4);
ssd1306_write_cmd(0xA6);
ssd1306_write_cmd(0xAF); ssd1306_clear(); return RT_EOK;
}
INIT_COMPONENT_EXPORT(rt_hw_ssd1306_init);

把 badapple 数据编译进固件(1)

首先想到的是最常用的方法,把 badapple 数据放在一个数组里,然后把该数组的内容写入 OLED 就行了,因为 badapple 数据是二进制,转成数组形式的话,放在代码里面,就相当于把二进制转成字符串,比如以下数据:

对于第一行,转成数组形式的话,如下:

uint8_t example[] = {0x23,0x78,0x33,0xb9,0x04,0x4b,0x13,0xb1,0x04,0x48,0xaf,0xf3,0x00,0x80,0x01,0x23};

如果数量不多的话,还好手动转换下,可是对于这个有 5 点几兆的,也就是有 5383168 字节,上百万个字节,手动转换肯定不实际,也没去找有没有什么现成工具可以用,我自己用 python 写了个小程序,把这个转换出来了,来感受下:

看了下,有30多万行,我电脑打开这文件都会有点卡顿,

经过这么一顿操作,我可是代码量超过 10万行的了,还是轻轻松松、随随便便就达到了。

把该文件放到工程里面,然后写个播放 badapple 的函数:

int badappple_test(int argc, char **argv)
{
uint32_t len =0,frame = 0,i=0,index = 0;
rt_tick_t start = rt_tick_get();
len = sizeof(badapple);
frame = len / 1024; for(i=0;i<frame;i++)
{
ssd1306_fill(&badapple[i * 1024]);
}
rt_tick_t end = rt_tick_get();
rt_kprintf("Frame:%d\r\n",frame);
rt_kprintf("start:%d, end:%d, use:%d\r\n",start,end,end-start);
rt_kprintf("1 s tick:%d\r\n",rt_tick_from_millisecond(1000));
}
MSH_CMD_EXPORT(badappple_test, badappple test);

开始的时候获取滴答定时器当前的值:

rt_tick_t start = 	rt_tick_get();

然后获取 badapple 数据对应的数组的长度,计算帧数:

    len = sizeof(badapple);
frame = len / 1024;

然后就是写屏了:

    for(i=0;i<frame;i++)
{
ssd1306_fill(&badapple[i * 1024]);
}

最后获取结束的时候的滴答定时器的值,用于计算帧率:

  rt_tick_t end = 	rt_tick_get();
rt_kprintf("Frame:%d\r\n",frame);
rt_kprintf("start:%d, end:%d, use:%d\r\n",start,end,end-start);
rt_kprintf("1 s tick:%d\r\n",rt_tick_from_millisecond(1000));

编译,看下编译出来的固件大小:

也是有 5 点几兆,最后运行效果为:

串口终端输出的信息:

可以看到播放持续了 23.238 秒,可以计算出帧率为:

5257 / 23.238 = 226.22

226 帧每秒,这会不会是全网用单片机播放 badapple 中帧率最高的呢,

把 badapple 数据编译进固件(2)

对于第一种方法,由于要经过转换,有点麻烦,突然想到之前看到过一套 GUI 源码,代码工程里面是直接把二进制进固件,忘记具体是怎么实现的,上网搜了下,实验了几次,居然做出来了,具体做法如下。

实现这个功能的关键是 incbin 指令,该指令是一条 arm 伪指令,功能是把一个文件包含到当前的源文件中,编译的时候会把该文件以二进制的形式编译进固件中。由于该指令是汇编,所以需要创建一个 .s 文件,内容为:

  .section .rodata
.global BADAPPLE_FILE
.type BADAPPLE_FILE, %object
.align 4
BADAPPLE_FILE:
.incbin "applications/badapple.bin"
BADAPPLE_FILE__END:
.global BADAPPLE_FILE_SIZE
.type BADAPPLE_FILE_SIZE, %object
.align 4
BADAPPLE_FILE_SIZE:
.int BADAPPLE_FILE__END - BADAPPLE_FILE

这里定义了 2 个全局变量 BADAPPLE_FILE、BADAPPLE_FILE_SIZE,BADAPPLE_FILE 是 badapple 数据开始位置,相当于一个数组头字节地址,BADAPPLE_FILE_SIZE,是 badapple 数据大小。把该 .s 文件 跟 badappel.bin 同时放到 工程中的 applications 目录下,还要修改下 applications 的 SConscript 文件,因为默认是没有编译该目录下的 .s 文件,修改为:

# RT-Thread building script for component

from building import *

cwd = GetCurrentDir()
src = Glob('*.c') + Glob('*.cpp') + Glob('*.s')
CPPPATH = [cwd, str(Dir('#'))] group = DefineGroup('Applications', src, depend = [''], CPPPATH = CPPPATH) Return('group')

接下里就实现播放 badapple 函数:

extern const uint8_t BADAPPLE_FILE;
const uint8_t *badapple_p = &BADAPPLE_FILE;
extern uint32_t BADAPPLE_FILE_SIZE;
int badappple_test(int argc, char **argv)
{
uint32_t frame = 0,i=0,index = 0;
rt_tick_t start = rt_tick_get();
frame = BADAPPLE_FILE_SIZE / 1024; for(i=0;i<frame;i++)
{
ssd1306_fille((uint8_t *)&badapple_p[i * 1024]);
}
rt_tick_t end = rt_tick_get();
rt_kprintf("file size is:%d",BADAPPLE_FILE_SIZE);
rt_kprintf("Frame:%d\r\n",frame);
rt_kprintf("start:%d, end:%d, use:%d\r\n",start,end,end-start);
rt_kprintf("1 s tick:%d\r\n",rt_tick_from_millisecond(1000));
}
MSH_CMD_EXPORT(badappple_test, badappple test);

编译运行,经过测试,效果跟上一个方法是一样的。

转载请注明出处:https://www.cnblogs.com/halin/

NUC980 运行 RT-Thread 驱动 SPI 接口 OLED 播放 badapple的更多相关文章

  1. RT Thread的SPI设备驱动框架的使用以及内部机制分析

    注释:这是19年初的博客,写得很一般,理解不到位也不全面.19年末得空时又重新看了RTThread的SPI和GPIO,这次理解得比较深刻.有时间时再整理上传. -------------------- ...

  2. Linux内核调用SPI平台级驱动_实现OLED的显示功能

    Linux内核调用SPI驱动_实现OLED显示功能 0. 导语 进入Linux的世界,发现真的是无比的有趣,也发现搞Linux驱动从底层嵌入式搞起真的是很有益处.我们在单片机.DSP这些无操作系统的裸 ...

  3. linux驱动基础系列--Linux下Spi接口Wifi驱动分析

    前言 本文纯粹的纸上谈兵,我并未在实际开发过程中遇到需要编写或调试这类驱动的时候,本文仅仅是根据源码分析后的记录!基于内核版本:2.6.35.6 .主要是想对spi接口的wifi驱动框架有一个整体的把 ...

  4. 自定义AXI总线形式SPI接口IP核,点亮OLED

    一.前言 最近花费很多精力在算法仿真和实现上,外设接口的调试略有生疏.本文以FPGA控制OLED中的SPI接口为例,重新夯实下基础.重点内容为SPI时序的RTL设计以及AXI-Lite总线分析.当然做 ...

  5. RT Thread 通过ENV来配置SFUD,操作SPI Flash

    本实验基于正点原子stm32f4探索者板子 请移步我的RT Thread论坛帖子. https://www.rt-thread.org/qa/forum.php?mod=viewthread& ...

  6. 嵌入式Linux设备驱动程序:在运行时读取驱动程序状态

    嵌入式Linux设备驱动程序:在运行时读取驱动程序状态 Embedded Linux device drivers: Reading driver state at runtime 在运行时了解驱动程 ...

  7. 联盛德 HLK-W806 (六): I2C驱动SSD1306 128x64 OLED液晶屏

    目录 联盛德 HLK-W806 (一): Ubuntu20.04下的开发环境配置, 编译和烧录说明 联盛德 HLK-W806 (二): Win10下的开发环境配置, 编译和烧录说明 联盛德 HLK-W ...

  8. 国产CPLD(AGM1280)试用记录——做个SPI接口的任意波形DDS [原创www.cnblogs.com/helesheng]

    我之前用过的CPLD有Altera公司的MAX和MAX-II系列,主要有两个优点:1.程序存储在片上Flash,上电即行,保密性高.2.CPLD器件规模小,成本和功耗低,时序不收敛情况也不容易出现.缺 ...

  9. 高通APQ8074 spi 接口配置

    高通APQ8074 spi 接口配置 8074 平台含有两个BLSP(BAM Low-Speed Peripheral) , 每一个BLSP含有两个QUP, 每一个QUP可以被配置为I2C, SPI, ...

随机推荐

  1. QT 资源链家暂存

    1.Qt右击菜单栏中文化 链接:https://blog.csdn.net/yangxiao_0203/article/details/7488967

  2. 【CentOS_7】一行shell实现自动清理过期日志

    昨日web测试环境登录白屏,慌忙登上机器查看,半天没找到问题. 不知哪根筋不对,df -h 一看 , /dev/sda1 已经100%. 立马 du -sh *,发现log日志有点大. 手工清理后,业 ...

  3. Xshell 远程使用vim打开文件不能使用右键复制粘贴(右键显示可视)的问题

    Xshell 远程使用vim打开文件不能使用右键复制粘贴(右键显示可视)的问题 Debian9.4系统不能再VIM打开文件界面不能使用右键复制粘贴 root@debian:~# vim /usr/sh ...

  4. LVM 相关知识

    LVM 相关知识 一.示例图 二.概念 名词 全称 释义 PV Physical Volume 物理硬盘.硬盘分区或者RAID磁盘阵列,先要创建pv VG Volume Group 卷组建立在物理卷之 ...

  5. nginx rewite重定向详解及实例解析

    静态和动态最大的区别是是否调用数据库. 什么是rewrite 将浏览器发送到服务器的请求重写,然后再返回给用户. 就是修改url,提高用户体验 rewrite的用途 80强转443 (优化用户体验) ...

  6. shell初学之nginx(域名)

    创建两个以域名区分的虚拟网站: 1 #!/bin/bash 2 curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/ ...

  7. 11.6 mpstat:CPU信息统计

        mpstat 是Multiprocessor Statistics的缩写,是一种实时系统监控工具.mpstat命令会输出CPU的一些统计信息,这些信息存放在/proc/stat文件中.在多CP ...

  8. 一文带你搞懂 RPC 到底是个啥

    RPC(Remote Procedure Call),是一个大家既熟悉又陌生的词,只要涉及到通信,必然需要某种网络协议.我们很可能用过HTTP,那么RPC又和HTTP有什么区别呢?RPC还有什么特点, ...

  9. Archlinux常用软件推荐 更新于2021年4月

    记录一下常用软件 必装软件 包管理工具 yay 代替pacman的包管理 yaourt 备用 终端工具 zsh oh-my-zsh-git 搭配zsh利器` proxychains4 终端代理工具` ...

  10. Apple macOS 下载汇总

    macOS Big Sur 11,macOS Catalina 10.15,macOS Mojave 10.14,macOS High Sierra 10.13,macOS Sierra 10.12 ...