历史上的今天
返回首页

历史上的今天

今天是: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库

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,延时几百纳秒,使用`提供的工具,延时精确到微秒,微秒数为参数除以10,由于除以10很慢,改为除以8;


#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);

推荐阅读

史海拾趣

DCX-CHOL Enterprises公司的发展小趣事

在追求经济效益的同时,DCX-CHOL Enterprises也积极履行社会责任,倡导绿色环保。公司采用环保材料和生产工艺,减少生产过程中的污染物排放。同时,公司还积极推广节能减排的理念,鼓励员工和合作伙伴共同参与环保行动。这些举措不仅提升了公司的社会形象,也为公司的可持续发展提供了有力保障。

GS Yuasa Battery Sales UK Ltd.公司的发展小趣事

DCX-CHOL Enterprises成立于一个科技蓬勃发展的时代。公司的创始人是一群热衷于电子技术的年轻人,他们看到市场上对于高性能、低功耗芯片的巨大需求,于是决定自主研发。经过数年的艰苦努力,他们成功开发出了一款具有革命性意义的低功耗芯片,该芯片不仅性能卓越,而且成本远低于同类产品。这一创新成果迅速赢得了市场的认可,DCX-CHOL Enterprises因此获得了第一桶金,为公司后续的发展奠定了坚实的基础。

Cyrix Corp公司的发展小趣事

Cyrix Corp公司成立于1988年,由Jerry Rogers和Tom Brightman创立。这两位创始人都是德州仪器的杰出思想家,他们雄心勃勃地希望挑战当时的芯片巨头英特尔。Cyrix的起步产品是高速x87数学协处理器,其性能比英特尔同类产品高出约50%,同时价格更为亲民。这一策略迅速赢得了市场的认可,Cyrix开始在芯片市场上崭露头角。

FEMA Electronics Corporation公司的发展小趣事

随着科技的不断发展,FEMA意识到只有不断创新才能在竞争激烈的市场中立于不败之地。因此,公司加大了研发投入,不断推出具有自主知识产权的新产品。其中,一款高性能的集成电路芯片在市场上引起了广泛关注。这款芯片不仅性能卓越,而且具有极高的性价比,迅速赢得了客户的青睐。FEMA凭借这一产品,成功打开了新的市场领域,实现了业务的快速增长。

方向电子公司的发展小趣事

FEMA的创始人李明(化名)是一位资深的电子工程师,他在一次与客户的交流中,发现了市场对高质量电子元件的迫切需求。于是,他毅然决定创办FEMA,专注于研发和生产高性能的电子元器件。创业初期,公司面临着资金短缺、技术瓶颈等重重挑战。然而,李明凭借坚定的信念和不懈的努力,成功攻克了技术难关,并与多家知名企业建立了合作关系,为公司的发展奠定了坚实的基础。

长运通(CYT)公司的发展小趣事

长运通注重与高校和研究机构的合作,积极引进和培养人才。公司与电子科技大学、西安电子科技大学等知名高校建立了紧密的合作关系,共同开展技术研发和人才培养。通过与高校的合作,长运通不仅获得了更多的技术资源支持,也为公司培养了一批高素质的研发人才。这些人才为公司的发展提供了强有力的支撑。

问答坊 | AI 解惑

vs2005 + wince6.0 链接错误(处理器类型)

..\\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 ...…

查看全部问答>

wireshark 可以抓取本机发给本机的数据报文吗?

听说是可以的,大家知道怎么设置吗?多谢了。…

查看全部问答>

wince6.0 作为复合设备识别(compositefn)问题

我在作wince6.0的驱动,用的是6410开发板。我想在pc端让板子被识别为一个串口和一个u盘。现在已经把PUBLIC\\DRIVER\\USBFN\\CLASS\\COMPOSITEFN驱动编成了compositefn.DLL,并且能够当做流驱动,在系统启动的时候加载成功了。 但是主机端设备管理 ...…

查看全部问答>

求此LCD与工控板连接方案

板子是研杨的4310,带44pin的LCD接口,接口定义如下: 01. +12 VDC          02. +12 VDC 03. GND              04. GND 05. +5 VDC     &nbs ...…

查看全部问答>

对Cortex-M3的中断嵌套时堆栈处理的疑问,望指教

看了Cortex-M3技术参考手册后,感觉对异常处理中堆栈的操作理解还是不清晰.1.在Thread mode下,发生异常或中断,处理器自动将xPSR,PC,LR,R12,R3,R2,R1,R0进行压栈,当ISR返回时,又自动将上述寄存器出栈.这个没问题.2.当抢先优先级不同时,优先 ...…

查看全部问答>

测电感简单方法有哪些

因为小的电感用电感表测试是测不出来,即使能测出来误差也相差很大,那请问还有别的方法吧。具体的跟我讲讲,最好有张原理图。谢谢各位高手 急需…

查看全部问答>

求助 波形的匹配

     各位大侠,大家好!本人菜鸟一枚,因最近要写毕业论文,所以要做一些实验。。要实现的就是,存储一组波形(几十个波形)作为波形库,然后输入一个波形,和波形库中波形比对,找出最相似的波形,最后在LCD上显示出来。请问这要 ...…

查看全部问答>

各位兄弟,这到底是怎么回事啊???

本帖最后由 paulhyde 于 2014-9-15 03:34 编辑 今天电赛的证书拿到手里了,我是队长+所有自购原件的买单者+电路绝大部分的设计制作者,为什么名字被放在三个人最后啊????????????????????????我们组是两个大三的带一个大 ...…

查看全部问答>

LPC2104_flash.icf或者LPC2104_ram.icf文件

最近在做ARM7LPC系列芯片的开发,在用IAR开发环境时缺少LPC2104_flash.icf或者LPC2104_ram.icf文件,哪位大侠有类似的配置文件?本人不胜感激。…

查看全部问答>