我开始涉足嵌入式开发工作,就像之前的所有工作一样,我的首要任务是使LED闪烁。我走得更远,制作了一个“运行器”,按顺序将每个LED从0点亮到7。电路板加电后,序列立即开始。您可以在此处看到它的运行情况。

我正在将ATxMegaA1芯片与avr-gcc编译器和工具链一起使用。我咨询最多的文档是“使用IO引脚”和“外部中断”文档。

我认为创建“ Led”概念是一个很好的抽象,可以将它们连接到板上的端口包裹起来。我还引入了一个宏来更改调试时的延迟,因为模拟器比真实芯片要慢几个数量级。

这是我有史以来第一个c语言,因此请保持野蛮。

led.h

#include <avr/io.h>

#define LEDPORT PORTE_OUT
#define LEDPORT_DIR PORTE_DIR

void init(void);
void toggleLights(int ledPosition);


led.c

#include "led.h"

void init(void)
{
    LEDPORT_DIR = 0b11111111; //Configure LED port for output
    LEDPORT = 0b11111111; //LEDs are active low, this makes sure they're off on start up
}

//Turns light at provided port pin on and all others off.
void toggleLights(int ledPosition)
{
    LEDPORT = ~(1 << ledPosition);
}


Blink.c

#include <stdint.h>
#include "led.h"

#ifdef DEBUG
    #define DELAYITERATIONS 0
#else
    #define DELAYITERATIONS 10000
#endif 

void delay(volatile uint32_t d)
{
    while (d-- != 0)     // loops while non-0 and decrements
    ;
}

int main(void)
{
    init();

    while(1)
    {
        for (int i = 0; i < 8; i++)
        {
            toggleLights(i);
            delay(DELAYITERATIONS);
        }
    }
}


评论

第一次尝试还不错。对于您的下一次迭代,我建议将其重新编码为计时器中断驱动的代码。

@Edward,关于该主题,您应该赞成这个答案。谢谢btw!

#1 楼

切换?还是设置?
我们的toggleLights函数的名称奇怪,不能满足我的期望。 (特别是,拨动开关会关闭和关闭内容。)
在C语言中,尤其是在嵌入式C语言中,我们不应该担心将无符号整数用作位数组。想要一个看起来像这样的setLights函数:
void setLights(uint8_t lights) {
    LEDPORT = lights;
}

同时一次打开八个灯,我们可以在我们的for循环中对它们进行位移位: br />因此,现在我们可以有效地执行您的代码尝试执行的操作,但是setLights函数更加灵活。它只是将灯光完全设置为我们要设置的灯光(可以说,该功能可能根本不需要存在...但是它确实将输入限制为正确的类型,并给了我们一些安全感)。 br />
使用typedef

首先,我们应该在这里使用uint8_t而不是int,因为我们永远不会使用任何我们无法用8位表示的值,并且这样可以节省24位。
但是我们如何使用自己的名称作为类型呢?我们的setLights循环变为:
for (uint8_t lights = 1; lights != 0; lights <<= 1) {
    setLights(lights);
    // delay
}

这种类型有助于增加此变量应该表示和使用的含义的清晰度。这在嵌入式C中特别有用,在嵌入式C中,您可能容易拥有大量for类型的变量(或其他大小的无符号整数)。 。每个人都知道并熟悉uint8_t,对吗?这是十个复合赋值运算符中最常见的。
同时,lights <<= 1是按位左移运算符(您在<<=函数中使用了它)。 >我们将+=初始化为<<。运行循环主体。更新语句将toggleLights向左移动forlights),最后得到:0x00000001
我们重复几次,直到到达lights,下一条update语句将所有位移出。我们以1结尾(只是lights <<= 1),并且退出0x00000010循环,因为0x10000000现在返回了0x00000000
而不是您的宏版本,我认为如果仅将其内联到循环中,则代码会更清晰:
typedef uint8_t light_positions;

当然,如果在多个位置重复使用0,那么您的版本就可以了-我们不想写额外的代码。但是,如果我们只在一个地方使用它,就可以这样写。这有助于使您的代码更具可读性)。选择一个并使用它。

十六进制与二进制
使用十六进制表示法比计算二进制表示法中的1的数量更具可读性。我想我宁愿看到for而不是lights != 0
您是否还注意到那里只有7个?

#2 楼

您当前的延迟代码正忙于等待,这意味着为了延迟所需的时间,您只是在浪费所需的许多CPU周期,以使该时间段过去。如果我没记错的话,_delay_ms是库函数,它执行的功能与您已有的功能相似。它通过基于给定时钟速度的延迟所需的ms来计算等待所需的无操作次数。 _delay_ms仍然是一个繁忙的等待功能,如果延迟时间大于最小延迟时间,您应该怀疑它的用法。人们在嵌入式工作中必须解决的问题芯片设计人员包括了许多带有中断的计时器,以解决这一确切问题。因此,实现此延迟的更好方法是使用提供给您的内部计时器。给定所需的预分频和延迟量,您将需要编写一些代码来设置计时器的所需计数器值。然后创建一个在定时器比较时触发的中断,当您要延迟时,必须启用该中断。然后在定时器中断的ISR中,您可以切换从那里驱动LED的引脚。这将节省大量CPU资源,并为您引入一个重要的概念,即在您的代码中需要计时器时,可以同时执行多个操作。

评论


\ $ \ begingroup \ $
你是完全正确的。我已经在计划下一步。
\ $ \ endgroup \ $
–RubberDuck
15年7月27日在14:28

\ $ \ begingroup \ $
@RubberDuck,太好了!这绝对是学习如何完成更高级别的嵌入式系统工作的重要一步。
\ $ \ endgroup \ $
–shuttle87
15年7月27日在14:30

\ $ \ begingroup \ $
@RubberDuck,因此,如果您将其发布为后续活动,我不会忘记在聊天中对我ping /提醒我,我希望看到它。
\ $ \ endgroup \ $
–shuttle87
15年7月27日在15:23

\ $ \ begingroup \ $
我肯定会忘记穿梭。不过,在此聊天室中有一个针对嵌入式问题的提要。
\ $ \ endgroup \ $
–RubberDuck
15年7月27日在15:34

#3 楼

幻数

您的代码中有一些幻数。为它们创建常量以使代码更具可读性和可维护性。使用未命名的enum是创建常量int值的技巧。在这种情况下,使用const限定变量可以工作,但不适用于switch语句之类的东西和数组大小。有关在C中声明常量的更多信息,请参见此答案。

enum {
    LIGHTS_COUNT = 8,
    LEDPORT_INIT_VALUE = 0b11111111
};


延迟

您正在运行繁忙的循环以延迟该过程。这样可以防止操作系统安排其他任务,而不是将资源用于无操作。请参见“繁忙等待”与“睡眠”之间的权衡取舍?我建议改用sleep-这是等待的普通方法。

按@RubberDuck的建议在其答案中使用_delay_ms。在C语言中,传统的无限循环使用for而不是while。有必要将其拆分为3个不同的文件吗?我认为,只有在模块更大的情况下,使用更多的源文件才有用。

评论


\ $ \ begingroup \ $
我打算使这个项目变得更大。我才刚刚开始。感谢您的反馈。
\ $ \ endgroup \ $
–RubberDuck
15年7月26日在20:42

\ $ \ begingroup \ $
为什么要#define可以声明为类型的常量?
\ $ \ endgroup \ $
– nhgrif
15年7月26日在21:16

\ $ \ begingroup \ $
我怀疑,除了样式首选项外,无限循环选择是任意的。 (stackoverflow.com/questions/885908/…)
\ $ \ endgroup \ $
– B. Wolf
17年4月7日在7:04

#4 楼

这确实是挑剔,挑剔,挑剔的,我也知道。请记住这一点。



led.h标头中没有任何有害内容,但是您没有包装它,因此它仅包含一次。您可以在各种头文件(例如stdio.h)中看到这种做法,并且这样做是为了避免当.h和.c文件都包含相同的头文件时,您不会编译错误。 >
#include <avr/io.h>

#ifndef _LED_H_
#define _LED_H_

#define LEDPORT PORTE_OUT
#define LEDPORT_DIR PORTE_DIR

void init(void);
void toggleLights(int ledPosition);

#endif



这是很挑剔的部分:

C语言的布尔测试被奇怪地处理了。我们倾向于将1视为真实,将0视为错误,但这并不完全正确。零是错误的,而不是零是不正确的。

因此,您的while循环:非常清晰。我明白这一点。但是

while(1)




while( -666 )


也是如此,这显然不太清楚。

真的,您要表达的是TRUEFALSE

您可以在C语言标头中到处找到这种定义:

有时:

while(8675309)


我更喜欢它,因为它消除了歧义。

总的来说,我同意上面所说的很多内容。



评论


\ $ \ begingroup \ $
除非编译器是老式的,否则也可以使用。另外,欢迎参加代码审查!
\ $ \ endgroup \ $
–爱德华
15年7月27日在21:26



\ $ \ begingroup \ $
我不相信stdbool.h可用于我的编译器,但我会检查。我确实不得不说我更喜欢TRUE!= FALSE,因为它更准确地反映了C如何评估布尔值的现实。
\ $ \ endgroup \ $
–RubberDuck
15年7月27日在21:49

\ $ \ begingroup \ $
乍一看,好像那里是stdbool。 avrfreaks.net/forum/stdbool-avr-libc
\ $ \ endgroup \ $
–RubberDuck
15年7月27日在21:53

#5 楼

与其他答案相比,这只是一个小问题,但对于初学者来说值得注意。这是LED头文件。


#include <avr/io.h>

#define LEDPORT PORTE_OUT
#define LEDPORT_DIR PORTE_DIR

void init(void);
void toggleLights(int ledPosition);



头文件中的任何内容都可用于外部代码。没有任何理由可以访问LEDPORT_DIR。我们要为输出设置此端口,仅此而已。初始化后,不要给自己一个机会去修改该设置。最少将其移至led.c文件,但实际上,它并没有做很多事情,可以完全删除。

当然,您仍然可以通过PORTE_DIR访问它,但是抽象的要点是,该模块应该是按其真实名称访问PORTE的唯一模块。 LEDPORT可以使用任何其他方法,并且应该没有设置该端口作为输入。添加delay并将其替换为#include <util/delay.h>