历史上的今天
今天是:2024年09月08日(星期日)
2020年09月08日 | AVR单片机教程——示波器
2020-09-08 来源:eefocus
在用DAC做了一个稍大的项目之后,我们来拿ADC开开刀。在本讲中,我们将了解0.96寸OLED屏,移植著名的U8g2库到我们的开发板上,学习在屏幕上画直线的算法,编写一个示波器程序,使用EEPROM加入人性化功能,最后利用示波器观察555定时器、放大电路、波形变换电路的各种波形。
OLED屏
我们使用的是0.96寸OLED屏,它由128*64个像素点构成,上16行为蓝色,下48行为黄色,两部分之间有大约两像素的空隙。虽然有两种颜色,但每个像素点都只能发出一种颜色的光,因此这块OLED屏算作单色屏。
可以插在开发板上的是显示屏模块,它由裸屏和PCB等组成,裸屏通过30 pin的排线焊接在PCB的反面。

在裸屏的内部有一块控制与驱动芯片,型号为SSD1315,与SSD1306兼容,它是外部与像素点之间的桥梁。SSD1315有200多个引脚,其中128个segment和64个common以动态扫描的方式驱动每一个像素点,这就是它为什么必须做在裸屏的内部。除了这些以外,它还有许多电源和控制引脚:
VDD是控制逻辑的供电,范围为1.65V到3.5V;VCC是OLED面板驱动电压,范围为7.5V到16.5V;VBAT是内部电荷泵的供电,范围为3.0V到4.5V,VBAT经电荷泵升压后提供给VCC,此时VCC需要连接电容到地;电荷泵需要两个外部电容,连接在C1P和C1N、C2P和C2N之间;VCOMH是一个内部电压,需要连接电容到地;VSS、VLSS、BGGND、LS都接地;IREF用于控制参考电压。
BS[2:0]用于选择接口模式,支持4线SPI、3线SPI、I²C、8位8080和6800;E(RD)和R/W(WR)在并行模式下使用;D[7:0]为数据,在SPI模式下,D0是时钟信号,D1是输入数据信号,D2连接D1或地;在I²C模式下,D0是时钟信号,D1和D2一起是数据信号;RES是复位信号;CS是片选信号;D/C用于指定输入是数据还是指令,在I²C模式下为地址选择,在3线SPI模式下保持低电平;FR、CL、CLS都是时钟信号。
看起来很复杂,但事实上有些信号根本不用管,因为裸屏只有30个引脚,去掉了BS2、E(RD)、R/W(WR)、D[7:3]、FR、CL、CLS,这些都是不常用的(除了FR帧同步信号,我觉得有点用)。剩下的你也许需要学,但不是现在,而是在你的项目需要用裸屏的时候,因为那块蓝色的PCB把这些都处理好了,只留下了7个引脚:GND、VCC、D0、D1、RES、DC、CS。可用的通信模式只有4线SPI、3线SPI和I²C,但已经相当丰富了,可以通过模块背面的电阻来选择,出厂时是4线SPI,也就是我们将要使用的模式。有的模块只支持I²C模式,也就只需要4个引脚了。
在4线SPI模式下,D0连接单片机USART1的XCK1,D1连接TXD1,CS连接PB2,这些是标准SPI的信号;RES连接PB0,D/C连接PB1。芯片在时钟上升沿采样数据信号,SPI模式0或3都可以使用。接下来我们来看总线上的数据。
当D/C为低时,总线上传输的是控制指令;当D/C为高时,总线上传输的是显示数据。64行被分为8页,芯片内部有1024字节的显存,每一字节对应一页中的一列,也就是纵向8个像素:

显存支持页面、水平、垂直三种寻址模式,伴随有一个指针,每写入一字节数据,指针就以某种形式增长,类似于我们在C中写的*ptr++:

芯片支持很多指令,它们的长度由第一个字节决定,有各自的格式,大致可以分为以下几类:
显存:寻址模式、行列地址、页面地址;
显示:起始行、显示行数、对比度、各种remap、全亮、反转、睡眠、偏移;
电源:IREF电流大小、VCOMH电压阈值、电荷泵开关;
时钟:时钟频率、时钟分频、预充电周期;
滚动:水平滚动、水平垂直滚动、滚动区域、启用禁用滚动;
高级:淡化、闪烁、放大。
对照着datasheet,我们来写几个指令,让屏幕亮起来。
#include #include #include void spi_init() { UCSR1B = 1 << TXEN1; UCSR1C = 0b11 << UMSEL10 #define UDORD1 2 | 0 << UDORD1 #define UCPHA1 1 | 0 << UCPHA1 | 0 << UCPOL1; set_bit(DDRD, 3); set_bit(DDRD, 4); } void spi_send(uint8_t _data) { UDR1 = _data; while (!read_bit(UCSR1A, TXC1)) ; set_bit(UCSR1A, TXC1); } void oled_init() { spi_init(); set_bit(DDRB, 0); // RES set_bit(DDRB, 1); // DC set_bit(DDRB, 2); // CS set_bit(PORTB, 2); // CS high set_bit(PORTB, 0); // RES high } void oled_control(uint8_t _size, ...) { reset_bit(PORTB, 1); // DC low reset_bit(PORTB, 2); // CS low va_list args; va_start(args, _size); for (uint8_t i = 0; i != _size; ++i) spi_send(va_arg(args, int)); va_end(args); set_bit(PORTB, 2); // CS high } void oled_data(uint16_t _size, const uint8_t* _data) { set_bit(PORTB, 1); // DC high reset_bit(PORTB, 2); // CS low for (const uint8_t* end = _data + _size; _data != end; ++_data) spi_send(*_data); set_bit(PORTB, 2); // CS high } int main(void) { oled_init(); oled_control(2, 0x8D, 0x95); // enable charge pump oled_control(1, 0xA1); // segment remap oled_control(1, 0xC8); // common remap oled_control(1, 0xAF); // display on uint8_t data[128]; for (uint8_t i = 0; i != 128; ++i) data[i] = i; for (uint8_t i = 0; i != 8; ++i) { oled_control(1, 0xB0 + i); oled_data(128, data); } while (1) ; } 先来看指令: 0x8D, 0x95启用内置电荷泵,将输出电压设置为9.0V; 0xA1和0xC8分别设置segment和common的remap,因为另一份datasheet中指明,显示屏的第一行连接Common 62,第一列连接Segment 127; 0xAF开启显示,显示是默认关闭的,需要手动开启; 0xB0到0xB7设置页面寻址模式下的页面地址,这是默认的寻址模式,我们在循环中先设置地址,再发送128字节的数据,内容是0到127,循环8次,把每一页都填满。 画出的是一个美丽的分形图: 再来看oled_control这个函数。参数列表的最后是...,表示可变参数。在函数调用时,匹配到...的参数需要用 va_list是一个类型,创建一个这个类型的变量,表示可变参数列表; va_start是一个宏,第一个参数为va_list变量,第二个为可变参数的数量; va_arg取出可变参数列表中的下一个变量,类型由第二个参数指定; va_end在使用完可变参数后做一些清理工作。 需要提醒的是,编译器无法检查标称的参数数量和类型与实际的是否符合。 U8g2是一个著名的单色显示屏驱动与图形库。“U”是universal,支持众多显示驱动芯片;“8”是8-bit,单片机与芯片以字节为单位通信;“g”是graphics,有绘制各种图形的函数;“2”是第二代。 文首的资料中包含了U8g2仓库的全部资料,下载于2020年2月9日,你也可以从GitHub上下载。C源代码在文件夹csrc中,包含头文件与实现。为了在我们的项目中包含这些文件,我们在Atmel Studio的Solution Explorer中对项目右键,点击Add→New Folder,命名为“u8g2”,然后右键它并点击Add→Existing Item,选择csrc中的文件,它们就会被拷贝到项目目录下,在代码中可以通过`#include U8g2的使用很简单,Wiki告诉我们,要首先创建u8g2_t类型的对象,随后每个函数的第一个参数都是它的指针。先根据显示屏的芯片型号选择合适的设置函数,初始化后就有那么多函数可以使用了。 U8g2没有提供SSD1315的驱动,但由于SSD1315与SSD1306兼容,我们可以选择u8g2_Setup_ssd1306_128x64_noname_f函数。后缀为_f的函数在RAM中设置了整个缓存,共128 * 64 / 8 = 1KB,这样用起来比较方便。 移植的核心就在于初始化时注册的两个回调函数。根据Wiki,我们要提供的两个函数的模板为: uint8_t u8x8_comm_callback(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) { switch (msg) { case U8X8_MSG_BYTE_INIT: break; case U8X8_MSG_BYTE_SET_DC: break; case U8X8_MSG_BYTE_START_TRANSFER: break; case U8X8_MSG_BYTE_SEND: break; case U8X8_MSG_BYTE_END_TRANSFER: break; default: return 0; } return 1; } uint8_t u8x8_gpio_delay_callback(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) { switch (msg) { case U8X8_MSG_GPIO_AND_DELAY_INIT: break; case U8X8_MSG_DELAY_NANO: break; case U8X8_MSG_DELAY_100NANO: break; case U8X8_MSG_DELAY_10MICRO: break; case U8X8_MSG_DELAY_MILLI: break; case U8X8_MSG_GPIO_CS: break; case U8X8_MSG_GPIO_DC: break; case U8X8_MSG_GPIO_RESET: break; default: return 0; } return 1; } 现在我们来一一填写其中的语句: U8X8_MSG_GPIO_AND_DELAY_INIT,初始化GPIO与延时; set_bit(DDRB, 0); set_bit(DDRB, 1); set_bit(DDRB, 2); U8X8_MSG_DELAY_NANO,延时若干纳秒,不超过100ns,由于CPU周期是40ns,函数调用的时间已经超过了100ns,因此什么都不做; U8X8_MSG_DELAY_100NANO,延时几百纳秒,使用` #define __DELAY_BACKWARD_COMPATIBLE__ #define F_CPU 25000000UL #include _delay_us(arg_int >> 3); U8X8_MSG_DELAY_10MICRO,延时几十微秒,同样使用_delay_us; _delay_us(arg_int * 10); U8X8_MSG_GPIO_CS、U8X8_MSG_GPIO_DC、U8X8_MSG_BYTE_INIT,分别设置CS、D/C、RES引脚电平,值为arg_int; case U8X8_MSG_GPIO_CS: cond_bit(arg_int, PORTB, 2); break; case U8X8_MSG_GPIO_DC: cond_bit(arg_int, PORTB, 1); break; case U8X8_MSG_GPIO_RESET: cond_bit(arg_int, PORTB, 0); break; 以上是第二个函数; U8X8_MSG_BYTE_INIT,通信的初始化,照搬spi_init函数就可以了; UCSR1B = 1 << TXEN1; UCSR1C = 0b11 << UMSEL10 #define UDORD1 2 | 0 << UDORD1 #define UCPHA1 1 | 0 << UCPHA1 | 0 << UCPOL1; set_bit(DDRD, 3); set_bit(DDRD, 4); U8X8_MSG_BYTE_SET_DC,设置D/C引脚的电平,这在上面已经写过了,可以通过u8x8_gpio_SetDC来转发; u8x8_gpio_SetDC(u8x8, arg_int); U8X8_MSG_BYTE_START_TRANSFER、U8X8_MSG_BYTE_END_TRANSFER,开始传输和结束传输,即拉低和拉高CS电平; case U8X8_MSG_BYTE_START_TRANSFER: u8x8_gpio_SetCS(u8x8, 0); break; case U8X8_MSG_BYTE_END_TRANSFER: u8x8_gpio_SetCS(u8x8, 1); break; U8X8_MSG_BYTE_SEND,发送数据,内容在arg_ptr中,大小为arg_int字节; for (const uint8_t* ptr = arg_ptr, *end = ptr + arg_int; ptr != end; ++ptr) { UDR1 = *ptr; while (!read_bit(UCSR1A, TXC1)) ; set_bit(UCSR1A, TXC1); UDR1; } 我们再来细品一下回调这个概念。U8g2库不知道OLED屏的各引脚该如何写高低电平,因为这些硬件操作是平台相关的,于是就得问我们如何实现这些操作。它问的方式不是人机交互,而是要求程序员在初始化时注册一个回调函数,这个函数的参数和返回值的类型,所谓接口,包括参数的含义,都由库定义,我们提供的函数是这个接口在我们平台上的实现。库拿到了这个函数以后,在任何它需要硬件操作的时候,就可以调用这个回调函数。通过注册与回调,我们解决了高层对低层的依赖,并使程序容易移植——如果要在另一个单片机平台上驱动OLED,只需重新实现接口,替换掉原来的回调函数,而库的代码可以完全复用。 但是回调是有一定代价的,原本可以调用确定的函数,或者直接内联,现在需要使用函数指针了。众所周知,指令也是数据,存储在flash中;函数是指令序列,它的第一个指令的地址就是函数指针的值。CPU中有一个特殊的寄存器,叫程序计数器(Program Counter,PC),它保存着CPU要执行的指令的地址;函数指针是变量,保存在寄存器中,用函数指针调用函数本质上是把寄存器的内容加载进PC中。 现代CPU都是多级流水线的,CPU在执行一条指令的同时,取指部件会将待执行的指令从flash中取出,这是因为flash的读取往往比CPU慢。但是,遇到从寄存器加载PC的指令时,取指部件不知道下一条指令的位置,必须等待CPU译码、执行后,才能根据PC去取指令,需要额外消耗几个CPU周期。好在这个消耗不大,并且CPU已经足够快,我们很少考虑函数指针与回调带来的overhead。事实上C++的虚函数就是用函数指针实现的,而C++是以运行时效率著称的编程语言。 然后我们就可以开心地画图了! #include #include #define __DELAY_BACKWARD_COMPATIBLE__ #define F_CPU 25000000UL #include #include #include "u8g2/u8g2.h" static u8g2_t u8g2; static uint8_t u8x8_comm_callback(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr); static uint8_t u8x8_gpio_delay_callback(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr); int main(void) { u8g2_Setup_ssd1306_128x64_noname_f(&u8g2, U8G2_R0, u8x8_comm_callback, u8x8_gpio_delay_callback); 

移植U8g2库
上一篇:AVR单片机教程——走向高层
下一篇:AVR单片机教程——DAC
史海拾趣
|
..\\Release\\blit_mmx.obj : fatal error LNK1112: module machine type \'X86\' conflicts with target machine type \'THUMB\' 其中blit_mmx.obj是由blit_mmx.asm 生成的,此文件编译命令行为: yasm -f win32 -o \"$(IntDir)/$(InputName).ob ...… 查看全部问答> |
|
wince6.0 作为复合设备识别(compositefn)问题 我在作wince6.0的驱动,用的是6410开发板。我想在pc端让板子被识别为一个串口和一个u盘。现在已经把PUBLIC\\DRIVER\\USBFN\\CLASS\\COMPOSITEFN驱动编成了compositefn.DLL,并且能够当做流驱动,在系统启动的时候加载成功了。 但是主机端设备管理 ...… 查看全部问答> |
|
板子是研杨的4310,带44pin的LCD接口,接口定义如下: 01. +12 VDC 02. +12 VDC 03. GND 04. GND 05. +5 VDC &nbs ...… 查看全部问答> |
|
看了Cortex-M3技术参考手册后,感觉对异常处理中堆栈的操作理解还是不清晰.1.在Thread mode下,发生异常或中断,处理器自动将xPSR,PC,LR,R12,R3,R2,R1,R0进行压栈,当ISR返回时,又自动将上述寄存器出栈.这个没问题.2.当抢先优先级不同时,优先 ...… 查看全部问答> |
|
本帖最后由 paulhyde 于 2014-9-15 03:34 编辑 今天电赛的证书拿到手里了,我是队长+所有自购原件的买单者+电路绝大部分的设计制作者,为什么名字被放在三个人最后啊????????????????????????我们组是两个大三的带一个大 ...… 查看全部问答> |
|
LPC2104_flash.icf或者LPC2104_ram.icf文件 最近在做ARM7LPC系列芯片的开发,在用IAR开发环境时缺少LPC2104_flash.icf或者LPC2104_ram.icf文件,哪位大侠有类似的配置文件?本人不胜感激。… 查看全部问答> |




