• AVR单片机教程——流水灯


    本文隶属于AVR单片机教程系列。

    上次我们用 delay 函数与 while 循环实现了一个LED的闪烁。这一次我们把所有LED加入进来,让它们依次闪烁,形成流水灯的效果。

    开发板上有4个LED,我们可以用不多的语句把循环体直接描述出来(看看就行,不用敲):

     1 led_set(LED_RED   , LED_ON);
     2 delay(250);
     3 led_set(LED_RED   , LED_OFF);
     4 led_set(LED_YELLOW, LED_ON);
     5 delay(250);
     6 led_set(LED_YELLOW, LED_OFF);
     7 led_set(LED_GREEN , LED_ON);
     8 delay(250);
     9 led_set(LED_GREEN , LED_OFF);
    10 led_set(LED_BLUE  , LED_ON);
    11 delay(250);
    12 led_set(LED_BLUE  , LED_OFF);

    但是如果LED多了怎么办?这样写一点都不优雅。一个更合理的方法是,用数组把LED存储起来,在无限循环中套循环,对LED进行遍历。代码如下:

     1 #include <ee1/led.h>
     2 #include <ee1/delay.h>
     3 
     4 int main()
     5 {
     6     led_init();
     7     const uint8_t leds[LED_COUNT] = {LED_RED, LED_YELLOW, LED_GREEN, LED_BLUE};
     8     while (1)
     9     {
    10         for (uint8_t i = 0; i < LED_COUNT; i++)
    11         {
    12             led_set(leds[i], LED_ON);
    13             delay(250);
    14             led_set(leds[i], LED_OFF);
    15         }
    16     }
    17 }

    在代码中,uint8_t 是一种无符号8位整数类型,定义在 <stdint.h> 中。相应地有 int8_t 表示带符号8位整数,以及 int16_t 、uint16_t 等,一直到64位。avr-gcc还提供了24位整型作为编译器扩展:__int24、 __uint24 ,但它们毕竟是编译器扩展,尽量别用;同时如果你的单片机程序中有16位整数搞不定的东西,那就应该考虑简化一下了。

    我们用的单片机是8位机,指令只能处理8位整数,如果是16位,则需要多条指令进行组合。而C语言内置类型 int 在这个环境中是16位的。因此,为了节省空间、提升性能,当一个数可以用8位表示时,应该使用 int8_t 或 uint8_t 代替 int 。

    讲了这么多,为什么LED可以用 uint8_t 表示呢?把光标移动到任一表示LED的宏上,右键,点击Goto Implementation,或按下快捷键Alt+G,你就能看到头文件中对这些宏的定义:

    1 #define LED_RED    (uint8_t)(0)
    2 #define LED_YELLOW (uint8_t)(1)
    3 #define LED_GREEN  (uint8_t)(2)
    4 #define LED_BLUE   (uint8_t)(3)

    同时,led_set 等函数都接受 uint8_t 类型的参数表示LED:

    1 void led_set(uint8_t _which, bool _on);
    2 void led_flip(uint8_t _which);

    这些都能说明为什么用 uint8_t 就能保存一个表示LED的宏。至于我为什么选择用 uint8_t ,当然是因为刚才说过的性能因素。

    顺便,我们可以发现表示LED状态的宏本质上就是 true 和 false ,表示LED的宏就是简单的0、1、2、3,因此程序中可以不用数组来存储LED,也不再需要任何宏,并且 true 和 false 也很容易对应到灯的亮和暗上:

     1 #include <ee1/ee.h>
     2 
     3 int main(void)
     4 {
     5     led_init();
     6     uint8_t i = 0;
     7     while (1)
     8     {
     9         led_set(i % 4, true);
    10         delay(250);
    11         led_set(i % 4, false);
    12         i++;
    13     }
    14 }

    第一行包含了头文件 ee.h ,相当于包含了库中所有的头文件。

    这样的程序是不是更好看一些?实际上 LED_RED 等宏的作用都只有让程序变得直观,如果直接用数字表示更方便,也完全没有问题。

    这里还用到一个小技巧来消除内层循环:在循环外设一个变量,每次循环让它加1,传入的参数由它本身改为它对4的模。

    你可能会想如果 i 过大了怎么办?当 i 等于255时, i++ 会让 i 变为0,而此时传入函数的参数还是从3回到0,符合控制逻辑。

    为什么程序没有 #include <stdbool.h> 就能直接使用 true 和 false ?因为你包含的头文件中用到了它们,这个头文件必定或直接或间接地包含了标准库头文件,因此你就可以直接用了。

    还有一个你可能疑惑的地方是,流水灯的逻辑本应是关闭上一个灯,打开下一个灯,然后延时一段时间,为什么在程序中是打开、延时,再关闭?想一想你会发现没什么不对,不过是把循环体的界限移动了一步。

    实际上,如果按前一种逻辑来写程序的话,循环体得写成这样:

    1 led_set((i + 3) % 4, false);
    2 led_set(i % 4, true);
    3 delay(250);

    这样虽然逻辑也正确,但反而更难理解。

    你也许还有疑问:把上一个灯关闭了以后,程序还要运行一会才能打开下一个灯,会不会看起来有一段时间是没有灯亮的?观察一下你的开发板,你就知道不会,原因也很简单,关灯和开灯之间的时间是微秒级的,人眼根本无法察觉。

    除了上述实现,流水灯还有一种效果,就是灯依次亮起,再依次关闭。留作今天的作业吧。

    上一篇:闪烁LED

    下一篇:烧写hex文件

  • 相关阅读:
    分布式事务slides
    为什么jdk中把String类设计成final
    Struts 体系结构与工作原理 图
    在指定路径或者是文件名查找指定的字符串
    Apache Avro 与 Thrift 比较
    SSL请求trustStore的两种注册方式
    java中的split使用的是正则表达式
    所有ghost操作系统大全
    Tomcat帮助文档翻译 未完成
    ORACLE基本配置
  • 原文地址:https://www.cnblogs.com/jerry-fuyi/p/11333257.html
Copyright © 2020-2023  润新知