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. [Java] 类库例题

    例1 字符串操作 定义一个StringBuffer类对象,然后通过append()方法向对象中添加26个小写字母,每次只添加一次,共添加26次,然后按逆序方式输出,并且可以删除前5个字符 面向过程实现 ...

  2. [Qt] 事件机制(四)

    滚轮事件:滚动滚轮实现窗口大小缩放 widget.h中增加: protected: void wheelEvent(QWheelEvent *event) Q_DECL_OVERRIDE; void ...

  3. Linux Test Project(一)

    http://www.vimlinux.com/lipeng/2014/09/12/ltp/ Testing Linux, one syscall at a time. LTP是从SGI开始的,后由I ...

  4. xl release 安装

    前提,该版本需要jdk-1.8以上(包括1.8),内存大于等于2G,磁盘最小空间2G,操作系统windows或者unix-family xl release是一个端到端的管道编排工具. 下载XL-re ...

  5. 015.Python函数名的使用以及函数变量的操作

    一 函数名的使用 python中的函数可以像变量一样,动态创建,销毁,当参数传递,作为值返回,叫第一类对象.其他语言功能有限 1.1 函数名是个特殊的变量,可以当做变量赋值 def func(): p ...

  6. Apache Jena Fuseki使用

    下载Apache Jena Fuseki 先从apache官网下载fuseki压缩包.然后解压到目标文件夹. apache官网:http://jena.apache.org/download/ 这里我 ...

  7. 常用Python第三方库简介

    如果说强大的标准库奠定了Python发展的基石,丰富的第三方库则是python不断发展的保证,随着python的发展一些稳定的第三库被加入到了标准库里面,这里有6000多个第三方库的介绍 下表中加粗并 ...

  8. 我眼中的WebViewJavascriptBridge(图解)

    工作中涉及到一段时间iOS开发,在开发中有个小小的框架让我非常深刻,就是WebViewJavascriptBridge,用于原生控件与前端交互通信.我觉得WebViewJavascriptBridge ...

  9. Go语言协程并发---管道信号量应用

    package main import ( "fmt" "math" "strconv" "time" ) /* ·10 ...

  10. Linux基础服务——Bind DNS服务 Part2

    Linux基础服务--Bind DNS服务 Part2 DNS反向解析与区域传送 实验环境延续Part1的实验环境. 反向区域配置 正向解析是域名到IP地址的映射,反向解析则是IP地址到域名的解析,在 ...