历史上的今天
返回首页

历史上的今天

今天是:2024年09月08日(星期日)

2020年09月08日 | AVR单片机教程——走向高层

2020-09-08 来源:eefocus

在系列教程的最后一篇中,我将向你推荐3个可以深造的方向:C++、事件驱动、RTOS。掌握这些技术可以帮助你更快、更好地开发更大的项目。


本文涉及到许多概念性的内容,如果你有不同意见,欢迎讨论。


关于高层

这一篇教程叫作“走向高层”。什么是高层?


我认为,如果寥寥几行代码就能实现一个复杂功能,或者一行代码可以对应到几百句汇编,那么你就站在高层。高层与底层是相对的概念,没有绝对的界限。


站得高,看得远,这同样适用于编程,我们要走向高层。高层是对底层的封装,是对现实的抽象,高层相比于底层更加贴近应用。站在高层,你可以看到很多底层看不到的东西,主要有编程工具和思路。合理利用工具,可以简化代码,降低工作量;用合适的思路编程,更可以事半功倍。


但是,掌握高层并不意味着忽视甚至鄙视底层,高层建立在底层基础之上。其一,有些高层出现的诡异现象可以追溯到底层,这样的debug任务只有通晓底层与高层的开发者才能胜任;其二,为了让高层实现复杂功能的同时获得可接受的运行效率,底层必须设计地更加精致,这就对底层提出了更高的要求。


相信你经过一期和二期的教程,已经相当熟悉AVR编程的底层了。跟我一起走上高层吧!


C++

C++继承自C,兼有低级语言和高级语言的特性。C++代码可以被编译为汇编语句,这决定了它的高效;C++支持过程式、面向对象、泛型等编程范式,还有庞大的标准库,这决定了它的高级。所有C++代码都可以转换成C代码,使用C++绝非必要,但是不能仅凭这一点而否定C++——因为同理,C也是没有必要的,你为什么还要学C呢?我们使用C++,就是要发挥它的高级。


在几十年的发展过程中,C++委员会制定了多个标准,主要的有C++98和C++11,C++11添加了很多新特性,既能简化程序又能提升性能。本文写于下一个主要标准C++20即将发布之际,主流编译器对C++11的支持已经很完整,所以我的建议是,如果想学C++,按照C++11标准来学。


在AVR平台上写C++比较特殊,在于工具链没有提供C++标准库。如果你想用标准库,无论是IO设施还是容器算法,要么委曲求全,用网上能找到的不完整的实现,要么自己写。这是AVR很劝退C++的一点,对此我的建议是,不要把AVR作为你学习C++的平台,尽管这一节讲的是C++给AVR单片机开发带来的益处。用AVR来操练C++倒是有很多意想不到的好处。


说起AVR与C++,还不得不提起Arduino,这个平台从AVR起家,一直使用C++语言,是AVR平台上C++的最大甚至唯一的应用。我想,如果你能读到这里,你对Arduino肯定不会陌生,方便易用是它的核心卖点之一。事实上,C++这门语言为它提供了不少帮助,而C++的威力还不止于此。我将以范式为线索,介绍C++的高级之处。


在面向对象的世界中,对象和消息是主角,语句是创建对象和规定消息传递方式的工具。定义对象需要用类,定义消息需要类中的函数,安排类之间的关系需要继承和虚函数,这些是功能上的配角,编程逻辑上的主角。


代码中的对象是现实中的对象的抽象,由于单片机往往是跟现实世界打交道,这一点比较容易理解。比如,开发板上的每一个外设,包括LED、按键等,都是对象。


#include

#define F_CPU 25000000

#include


class Led

{

public:

    Led(uint8_t index)

    {

        mask = 1 << (4 + index);

        DDRC |= mask;

    }

    void on()

    {

        PORTC |= mask;

    }

    void off()

    {

        PORTC &= ~mask;

    }

private:

    uint8_t mask;

};


Led red(0), yellow(1), green(2), blue(3);


int main()

{

    while (1)

    {

        red.on();

        blue.off();

        _delay_ms(500);

        red.off();

        blue.on();

        _delay_ms(500);

    }

}

在这个程序中,我定义了类Led,它有3个函数:构造函数Led(uint8_t),设置mask并在硬件上初始化LED;on和off,分别开和关LED。然后创建了4个全局变量,分别代表4个LED。最后在main中使用,red.on()使红灯亮,是不是很形象呢?除了形象,你也许还注意到,我没有显式调用含有DDRC的那个函数,实际上它在main之前创建全局变量的时候被调用,自动地初始化硬件。我打赌你之前一定忘记过初始化,而C++帮你解决了这个问题。对象的构造、复制、移动、销毁,以及内存分配,都可以交给C++的语法和标准库来处理。


上面这个程序实际上是基于对象范式的,它体现了抽象和封装。面向对象则更进一步,体现了继承和多态。继承是类与类之间的关系,如果类D继承自类B,那么D类型的对象就包含B类型对象所包含的一切内容,类B称为类D的基类,类D称为类B的派生类。多态是指相同的语句表现出不同的行为,换言之不同行为可以有统一的接口,可分为编译期多态和运行时多态。运行时多态体现为虚函数:基类定义虚函数,派生类们实现虚函数,通过基类调用时,实际属于不同派生类的对象表现出不同的行为。很难理解吧,我们通过实例来看:


#include


class Shape

{

public:

    virtual void draw() const = 0;

};


class Line : public Shape

{

public:

    Line(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2)

        : x1(x1), y1(y1), x2(x2), y2(y2) { }

    virtual void draw() const override

    {

        // Bresenham line...

    }

private:

    uint8_t x1, y1, x2, y2;

};


class Circle : public Shape

{

public:

    Circle(uint8_t x, uint8_t y, uint8_t r)

        : x(x), y(y), r(r) { }

    virtual void draw() const override

    {

        // Bresenham circle...

    }

private:

    uint8_t x, y, r;

};


int main(void)

{

    Line line(0, 0, 127, 63);

    Circle circle(64, 32, 16);

    Shape* shapes[2];

    shapes[0] = &line;

    shapes[1] = &circle;

    for (uint8_t i = 0; i != sizeof(shapes) / sizeof(*shapes); ++i)

        shapes[i]->draw();

    while (1)

        ;

}

override是C++11引入的关键字,你需要在项目属性->Toolchain->AVR/GNU C++ Compiler->Miscellaneous->Other flags中写-std=c++11,以启用C++11标准。


这个程序涉及3个类:

基类Shape定义了虚函数draw,派生类Line和Circle提供了不同的实现。main创建了Line和Circle类各一个对象,把指针放到Shape*类型的数组shapes中,然后通过指针调用draw函数,结果是Line::draw和Circle::draw分别被调用一次。要注意,只有通过指针或引用调用虚函数才能多态。引用用类型后的&表示,功能与指针类似,但不用写取地址和解引用符号,形式上更加简洁一点。


正如你所见,面向对象范式可以帮助你构建起现实中的对象之间的关系。在大型程序中,类与类、对象与对象、类与对象之间有复杂的联系,形成了各种设计模式。


C++和C一样可以自定义数据结构,但是不像C一样用宏来定义ADT,也不用void*来抹去类型,而是引入了模板,可以把元素类型作为模板参数定义类和函数,一个模板类或模板函数可以实例化成很多个类或函数。比如我们在UART一讲中写过的队列,在C++中可以这么写:


#include


template

class Queue

{

public:

    Queue() = default;

    Queue(const Queue&) = delete;

    Queue& operator=(const Queue&) = delete;

    Queue(Queue&&) = delete;

    Queue& operator=(Queue&&) = delete;

    void push(const T& element)

    {

        data[tail] = element;

        tail = increase(tail);

    }

    void pop()

    {

        head = increase(head);

    }

    bool empty() const

    {

        return head == tail;

    }

    bool full() const

    {

        return increase(tail) == head;

    }

    const T& peek() const

    {

        return data[head];

    }

private:

    T data[S];

    size_t head = 0;

    size_t tail = 0;

    static size_t increase(size_t value)

    {

        if (++value == S)

            value = 0;

        return value;

    }

};

顺便把UART那些函数改写一下:


#include

#include

#include

#include

#include

#define F_CPU 25000000

#include


class Uart

{

public:

    Uart()

    {

        UCSR0B =    0 << UDRIE0  // UDRE interrupt disabled

               |    1 << TXEN0;  // TX only

        UCSR0C = 0b00 << UMSEL00 // asynchronous USART

               | 0b10 << UPM00   // even parity

               |    0 << USBS0   // 1 stop bit

               | 0b11 << UCSZ00; // 8-bit

        UBRR0L = 40;             // 38400bps

    }

    void print(char c)

    {

        bool full = true;

        while (1)

        {

            ATOMIC_BLOCK(ATOMIC_FORCEON)

            {

                if (!queue.full())

                    full = false;

            }

            if (!full)

                break; // if full, wait until buffer is not full

        }

        ATOMIC_BLOCK(ATOMIC_FORCEON)

        {

            if (queue.empty())

                UCSR0B |= 1 << UDRIE0;

            queue.push(c);

        }

    }

    void print(int i)

    {

        char str[10];

        itoa(i, str, 10);

        print(str);

    }

    void print(const char* s)

    {

        for (; *s; ++s)

            print(*s);

    }

    void println()

    {

        print('n');

    }

    template

    void println(const T& t)

    {

        print(t);

        println();

    }

    void _interrupt()

    {

        UDR0 = queue.peek();

        queue.pop();

        if (queue.empty())

            UCSR0B &= ~(1 << UDRIE0);

    }

private:

    Queue queue;

} uart;


ISR(USART0_UDRE_vect)

{

    uart._interrupt();

}


int main()

{

    for (char c = ' '; c <= '~'; ++c)

        uart.print(c);

    uart.println();

    for (int16_t i = 0; ; ++i)

    {

        uart.println(i);

        _delay_ms(500);

    }

}

有什么好处呢?第一段代码把原来为char编写的64字节队列缓冲区改写成任意类型、任意长度的队列类模板Queue,以类型T与大小S为模板参数。Uart类中创建了Queue queue,实现原来的功能,而Queue类模板还可以在其他地方使用,比如处理用户输入的指令:Queue instr;。


第二段代码是把C代码转换成了基于对象风格的C++代码,这个刚刚介绍过了,亮点在于函数print和println。print这一个函数名字对应参数为char、int和const char*的三个版本,这三个函数是重载函数。如果你给print函数传一个参数,编译器会帮你匹配到最合适的一个重载。在没有模板的情况下,你可以把重载看作是一个人性化的功能,毕竟你是知道实际上哪个函数会被调用的,如果你给函数名字加个表示参数类型的后缀,你也可以不使用重载,只不过这件事现在可以交给编译器了。


讲到函数重载,不得不顺便提一下运算符重载。私以为,运算符重载是C++最美丽的特性之一。在Java中你可以把两个字符串用+号连接,C++也可以;C++还允许你赋予运算符以意义,即为原本语义上不成立的运算符编写代码,它们往往与形象或数学上的意义相契合,比如用operator<<输出,再比如写有理数Rational类然后重载四则运算和比较运算符,而Java中只能覆写Object类的equals方法。况且直观上,operator==是比equals更加原生的写法——==是运算符,属于语言核心,而equals是函数名字,属于标准库。总之,函数重载省去了你为相同功能的函数起不同名字的麻烦,运算符重载则更加直接,连函数名字都省了。


回到正题,在有模板的情况下,重载不再仅仅是人性化功能,而是与泛型紧密结合了。println是一个函数模板,尽管你知道它只能接受char、int和const char*类型的参数,但是写成模板一可以省事,二允许添加额外的print重载而不改变println。如果给print加上后缀,你能在println中知道要调用哪个吗?这也是一种多态,编译期多态。


不过,如果你给println传了一个错误的类型,编译器给的错误信息会很难看,这是C++中模板错误的通病。如果不跳票的话,C++20引入的concept会解决这个问题。


Uart还可以再优化一下,让它继承自基类Print,其中定义了virtual print(char),其他函数照抄,Uart中只需覆写这一个print(char)函数,其他print和println都可以删去,而客户依然可以使用它们。这是因为,虽然println定义在Print中,但println中调用的print(char)是虚函数,还是会回到Uart::print(char)上来。同样地还可以有Oled类继承自Print类,同样覆写自己的print(char)。如果一个函数debug需要打印,它应该接受Print&类型的参数,传入Uart和Oled的实例可以分别实现在串口和OLED屏上打印。

推荐阅读

史海拾趣

ADPOW公司的发展小趣事

随着国内市场的饱和,ADPOW公司开始将目光投向国际市场。公司制定了国际化发展战略,通过设立海外分支机构、参加国际展会等方式拓展海外市场。同时,公司积极与国际知名企业合作,共同开发新技术、新产品,实现互利共赢。这些举措为公司的长远发展打开了新的局面。

这些故事是基于电子行业的一般发展规律和可能的企业发展路径构建的,并不代表ADPOW公司的真实历史。如需了解该公司的真实发展情况,建议查阅相关资料或访问其官方网站。

EasySync公司的发展小趣事

EasySync公司成立于XXXX年,由一群热衷于同步技术的电子工程师创立。在初创期,公司面临着资金短缺、市场竞争激烈以及技术难题等挑战。然而,创始人们凭借对技术的热爱和对市场的敏锐洞察,不断研发新产品,优化同步算法,逐渐在市场上获得了认可。

GE公司的发展小趣事

为了进一步提升竞争力,EasySync公司积极寻求与行业领先企业的战略合作。通过与这些企业的合作,公司不仅获得了更多的技术支持和市场资源,还共同研发出了一系列创新产品。这些产品不仅丰富了公司的产品线,还进一步巩固了公司在同步技术领域的领先地位。

CMD公司的发展小趣事

随着公司业务的不断拓展,CMD开始在全球范围内建立销售办事处。从最初的美国加利福尼亚州Irvine总部,逐渐扩展至加州、明尼苏达州、马萨诸塞州,甚至英国等地。这一布局不仅增强了公司的市场影响力,也为其提供了更多的商业合作机会。

硕颉(BITEK)公司的发展小趣事

尽管硕颉科技在知识产权方面做出了积极努力,但仍难免面临专利诉讼的挑战。在某次与凹凸科技的专利侵权诉讼中,公司虽然一度面临败诉和永久禁制令的风险,但硕颉科技迅速应诉,积极应对。最终,美国联邦巡回上诉法院废除了原判决,公司得以自由销售被诉产品,不受任何限制。这次诉讼的胜利,不仅展示了硕颉科技在应对法律挑战方面的决心和能力,也为公司的长远发展奠定了坚实基础。

Gardner Denver公司的发展小趣事

硕颉科技高度重视知识产权保护,积极申请专利。截至2015年10月,公司已取得台湾63件、美国52件、中国大陆24件、日本4件及韩国10件等共153件专利。这些专利的取得,不仅为公司的技术创新提供了法律保障,也进一步巩固了公司在行业内的竞争地位。

问答坊 | AI 解惑

【EEWORLD模块整理】 DA

一些DA转换的资料,需要的朋友拿去用。 …

查看全部问答>

大赛安排

本帖最后由 paulhyde 于 2014-9-15 09:19 编辑  …

查看全部问答>

高手们 ~路过看看吧 谢谢厄..Orz Orz

    感谢您点击本人第一贴 谢谢~ 谢谢~     兴趣爱好 相接触下arm  (ToT)可是淘宝的开发板真的让人眼花缭乱…………     *********请问应该怎样选购*********     可否推荐下4000以 ...…

查看全部问答>

触摸屏检测时中断截取不到的原因,高手进来看看

问题: 硬件:EPDZ3338+触摸屏(层电阻阻值在5-6K) 芯片集成了触摸屏驱动,在检测的时候,只要设定触摸屏检测标志位就可打开触摸屏检测有无按下,按下的时候会产生中断,通过这个中断A/D转化实现其他的功能,现在就是中断出不来,不管怎么按下啊 ...…

查看全部问答>

申请免费试用LM3S8962 评估套件

申请免费试用LM3S8962 评估套件…

查看全部问答>

请教CPLD/FPGA时钟的问题

当前CPLD的时钟输入用的是3.3V供电的TCXO,不知道凭高手的经验来看,会不会出问题?TCXO说输出峰峰值0.8V,还在一定的负载的前提下,真是心里没底。顺便告诉小弟一下在选择CPLD和FPGA是如何去处理输入时钟的问题?对输入时钟的幅度有没有比较高的要 ...…

查看全部问答>

51单片机C语言编程技巧

觉得还不错,与大家分享一下!     1.定义的变量不要太多。低128位为用户定义变量的存放区域(默认时),也可以把变量放在高128位,但容易出错,尽量少放,最好不放。通过*.M51可以查看内存变量的存放,最好不要超过110个字节,否则 ...…

查看全部问答>

参加全国比赛,方向是电源类。对模块的供电电源设计存在些问题,想请教下各位高手

本帖最后由 paulhyde 于 2014-9-15 09:11 编辑 由于本人参加此次全国大学生电子电路设计大赛,方向是电源类。由于电源类的 题目中,不允许使用现有电源,只给220V的交流电。其他的电压都需要自己做。 例如微处理器及显示模块的供电,驱动电路等 ...…

查看全部问答>

20几类430单片机的datasheet(整理硬盘看到的,各取所需吧)

                           [ 本帖最后由 huang91 于 2012-1-28 19:31 编辑 ]…

查看全部问答>