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. 神奇的不可见空格<200b>导致代码异常

    故事是这样发生的,在做一个JSON对象转化的时候,出现了转化异常:刚开始还是以为是格式错误,后来一步步排除,才发现是不可见空格<200b>导致的解析异常 出现 使用Typora编写文字时, ...

  2. [刷题] 1016 部分A+B (15分)

    思路 以字符串形式接收 遍历字符串,组装数据,输出结果 #include <iostream> using namespace std; int main() { string a, b; ...

  3. 用python输出未来时间,递增

    输入当前时间,之前与之后的365天时间日期 按格式化输出 #!/usr/bin/evn python # -*- coding: UTF-8 -*- # import time import date ...

  4. WPS-插入-公式-菜单 怎样在EXCEL中使用PRODUCT函数

    怎样在EXCEL中使用PRODUCT函数 ################   WPS2018 -插入-公式-[专门有公式菜单] 插入函数       ##################       ...

  5. linux系统开机自动挂载光驱 和 fstab文件详解

    Linux 通过 UUID 在 fstab 中自动挂载分区 summerm6关注 2019.10.17 16:29:00字数 1,542阅读 607 https://xiexianbin.cn/lin ...

  6. nosql数据库之Redis集群

    Redis 集群是一个可以在多个 Redis 节点之间进行数据共享的设施(installation). Redis 集群不支持那些需要同时处理多个键的 Redis 命令, 因为执行这些命令需要在多个 ...

  7. 如何查看自己的电脑 CPU 是否支持硬件虚拟化

    引言 在你安装各种虚拟机之前,应该先测试一下自己的电脑 CPU 是否支持硬件虚拟化. 如果你的电脑比较老旧,可能不支持硬件虚拟化,那么将无法安装虚拟机软件. 如何查看自己 CPU 是否支持硬件虚拟化 ...

  8. WEB安全防护相关响应头(上)

    WEB 安全攻防是个庞大的话题,有各种不同角度的探讨和实践.即使只讨论防护的对象,也有诸多不同的方向,包括但不限于:WEB 服务器.数据库.业务逻辑.敏感数据等等.除了这些我们惯常关注的方面,WEB ...

  9. JVM-垃圾收集算法基础

    目录 目录 前言 手动释放内存导致的问题 垃圾判定方法 哪些对象是垃圾? 引用计数算法 可达性分析法 垃圾收集算法 标记-清除 优点 缺点 优化 标记-复制 优点 缺点 优化 标记-整理 优点 缺点 ...

  10. Step By Step(Lua输入输出库)

    Step By Step(Lua输入输出库) I/O库为文件操作提供了两种不同的模型,简单模型和完整模型.简单模型假设一个当前输入文件和一个当前输出文件,他的I/O操作均作用于这些文件.完整模型则使用 ...