将ATmega328置于非常深的睡眠中并收听串行信号?


13

我研究了ATmega328的睡眠选项,并阅读了一些有关它的文章,我想了解是否还有更多选项。

因此,我希望获得尽可能低的电流,以便任何小于100uA的电流都是好的-只要我能听醒uart和中断即可。

我正在使用带有ATmega328p的定制PCB(不是UNO)。

将芯片设置为深度睡眠:

 set_sleep_mode (SLEEP_MODE_PWR_DOWN);  
 sleep_enable();
 sleep_cpu ();

根据与串行通信不会唤醒它,这个

您需要将其置于IDLE模式下以侦听串行,但这会消耗几个mA -bad。

我找到了此链接,您可以在其中将该硬件串行连接到中断-这很危险,因此您可能会丢失数据,此外,我需要这两个中断引脚。

我还阅读了Gammon的这篇文章,您可以在其中禁用某些功能,以便以较低的功耗获得IDLE睡眠-但他没有提及您从中得到的准确程度:

 power_adc_disable();
      power_spi_disable();
      power_timer0_disable();
      power_timer1_disable();
      power_timer2_disable();
      power_twi_disable();

那么,最重要的是,在没有任何硬件操纵的情况下,是否有任何选择,至少可以获得小于0.25mA的电流,并且还可以监听串行端口?例如,唤醒长串行数据输入?


1
@NickAlexeev这是一个ATmega328问题,而不是Arduino问题,因为它直接处理远远低于Arduino水平的芯片。停止不正确的迁移!
克里斯·斯特拉顿

1
几乎不。想要从睡眠中唤醒Arduino并不能真正消除它,因为它具有ATmega328芯片。以这种速度,您将能够将有关Arduino的所有问题弹回到EE站点。
尼克金门

Answers:


11

我们制作的木板可以做到这一点。

  • RX引脚连接到INT0
  • INT0引脚设置为输入或输入上拉,具体取决于RX线的驱动方式
  • 睡眠时,启用INT0低电平中断

    //Clear software flag for rx interrupt
    rx_interrupt_flag = 0;
    //Clear hardware flag for rx interrupt
    EIFR = _BV(INTF0);
    //Re-attach interrupt 0
    attachInterrupt(INT_RX, rx_interrupt, HIGH);
    
  • INT0中断服务程序设置一个标志并禁用该中断

    void rx_interrupt()
    {
        detachInterrupt(INT_RX);
        rx_interrupt_flag = 1;
    }
    
  • 唤醒时,我们检查该标志(还有其他中断源)

在通讯方面,我们使用具有起始字符>和结束字符的消息协议\r。例如>setrtc,2015,07,05,20,58,09\r。这为防止丢失消息提供了一些基本的保护,因为传入的字符直到>收到a才被处理。为了唤醒设备,我们在传输之前发送了一条虚拟消息。一个字符就能做到,但我们会发送>wakeup\r嘿嘿。

在收到新消息的情况下,设备在接收到最后一条消息后会保持唤醒状态30秒钟。如果收到新消息,则将重置30秒计时器。PC接口软件每秒发送一条虚拟消息,以使设备在用户连接进行配置等时保持唤醒状态。

这种方法完全没有问题。带有一些外设的电路板在睡眠时消耗约40uA的电流。ATMega328P实际消耗的电流可能约为4uA。

更新资料

从数据表中可以看出,RX引脚也是引脚更改中断引脚16(PCINT16)

因此,另一种没有电线的方法可能是

  • 睡眠前:在PCMSK2中为PCINT16设置端口更改中断屏蔽位,清除PCIFR中的引脚更改端口2标志,通过在PCICR中设置PCIE2来启用引脚更改端口2中断(PCINT16-PCINT23)。

  • 为引脚更改端口2中断设置一个ISR,然后像以前一样继续操作。

关于端口更改中断的唯一警告是该中断在为该端口使能的所有8个引脚之间共享。因此,如果为端口启用了多个引脚更改,则必须确定是哪个触发了ISR中的中断。如果您不在该端口上使用任何其他引脚更改中断(在这种情况下为PCINT16-PCINT23),则这不是问题。

理想情况下,这就是我本来可以设计我们的电路板但我们可以进行工作的方式。


非常感谢 。除了硬件技巧,没有别的办法了吗???所以你只用1条线将rx连接到int0 / int1?
Curnelious

1
实际上,我只是看了一下数据表,您也许可以使用引脚更改中断
geometrikal

谢谢,会有什么不同?无论如何,我将不得不在int1上用rx唤醒?
Curnelious

您只需要1个中断引脚即可。我在上面发布了更多内容-您可以将RX引脚用作引脚更改中断。不过我还没有这样做,所以可能会有一些问题,例如,您可能必须在睡眠之前禁用RX /启用引脚更改,并在唤醒后禁用引脚更改/启用RX
geometrikal

谢谢,我不确定将rx连接到INT1为何会出现问题,而不是将int1发生时禁用中断,然后在进入睡眠状态时再次启用它们,将中断设置为高电平?
Curnelious

8

下面的代码实现了您的要求:

#include <avr/sleep.h>
#include <avr/power.h>

const byte AWAKE_LED = 8;
const byte GREEN_LED = 9;
const unsigned long WAIT_TIME = 5000;

ISR (PCINT2_vect)
{
  // handle pin change interrupt for D0 to D7 here
}  // end of PCINT2_vect

void setup() 
{
  pinMode (GREEN_LED, OUTPUT);
  pinMode (AWAKE_LED, OUTPUT);
  digitalWrite (AWAKE_LED, HIGH);
  Serial.begin (9600);
} // end of setup

unsigned long lastSleep;

void loop() 
{
  if (millis () - lastSleep >= WAIT_TIME)
  {
    lastSleep = millis ();

    noInterrupts ();

    byte old_ADCSRA = ADCSRA;
    // disable ADC
    ADCSRA = 0;  
    // pin change interrupt (example for D0)
    PCMSK2 |= bit (PCINT16); // want pin 0
    PCIFR  |= bit (PCIF2);   // clear any outstanding interrupts
    PCICR  |= bit (PCIE2);   // enable pin change interrupts for D0 to D7

    set_sleep_mode (SLEEP_MODE_PWR_DOWN);  
    power_adc_disable();
    power_spi_disable();
    power_timer0_disable();
    power_timer1_disable();
    power_timer2_disable();
    power_twi_disable();

    UCSR0B &= ~bit (RXEN0);  // disable receiver
    UCSR0B &= ~bit (TXEN0);  // disable transmitter

    sleep_enable();
    digitalWrite (AWAKE_LED, LOW);
    interrupts ();
    sleep_cpu ();      
    digitalWrite (AWAKE_LED, HIGH);
    sleep_disable();
    power_all_enable();

    ADCSRA = old_ADCSRA;
    PCICR  &= ~bit (PCIE2);   // disable pin change interrupts for D0 to D7
    UCSR0B |= bit (RXEN0);  // enable receiver
    UCSR0B |= bit (TXEN0);  // enable transmitter
  }  // end of time to sleep

  if (Serial.available () > 0)
  {
    byte flashes = Serial.read () - '0';
    if (flashes > 0 && flashes < 10)
      {
      // flash LED x times 
      for (byte i = 0; i < flashes; i++)
        {
        digitalWrite (GREEN_LED, HIGH);
        delay (200);  
        digitalWrite (GREEN_LED, LOW);
        delay (200);  
        }
      }        
  }  // end of if

}  // end of loop

我在Rx引脚上使用了引脚更改中断来通知串行数据何时到达。在此测试中,如果5秒钟后没有任何活动,则板将进入睡眠状态(“清醒” LED熄灭)。传入的串行数据导致引脚更改中断唤醒板卡。它寻找一个数字,并使“绿色” LED闪烁该次数。

实测电流

在5 V电压下运行时,我在睡眠状态下测得的电流约为120 nA(0.120 µA)。

唤醒讯息

然而,问题在于,由于串行硬件期望Rx(起始位)的下降电平已经丢失,而该字节在完全唤醒时已经到达,因此丢失了第一个到达的字节。

我建议(如geometrikal的回答),您首先发送“唤醒”消息,然后暂停一小段时间。暂停是为了确保硬件不会将下一个字节解释为唤醒消息的一部分。之后,它应该可以正常工作。


由于这使用了引脚更改中断,因此不需要其他硬件。


使用SoftwareSerial的修订版本

下面的版本成功处理了串行接收的第一个字节。它通过以下方式做到这一点:

  • 使用使用引脚更改中断的SoftwareSerial。由第一个串行字节的起始位引起的中断也会唤醒处理器。

  • 设置保险丝,以便我们使用:

    • 内部RC振荡器
    • BOD已禁用
    • 保险丝为:低:0xD2,高:0xDF,扩展:0xFF

受FarO的启发,它可以使处理器在6个时钟周期(750 ns)内唤醒。在9600波特时,每个位时间为1/9600(104.2 µs),因此额外的延迟微不足道。

#include <avr/sleep.h>
#include <avr/power.h>
#include <SoftwareSerial.h>

const byte AWAKE_LED = 8;
const byte GREEN_LED = 9;
const unsigned long WAIT_TIME = 5000;
const byte RX_PIN = 4;
const byte TX_PIN = 5;

SoftwareSerial mySerial(RX_PIN, TX_PIN); // RX, TX

void setup() 
{
  pinMode (GREEN_LED, OUTPUT);
  pinMode (AWAKE_LED, OUTPUT);
  digitalWrite (AWAKE_LED, HIGH);
  mySerial.begin(9600);
} // end of setup

unsigned long lastSleep;

void loop() 
{
  if (millis () - lastSleep >= WAIT_TIME)
  {
    lastSleep = millis ();

    noInterrupts ();

    byte old_ADCSRA = ADCSRA;
    // disable ADC
    ADCSRA = 0;  

    set_sleep_mode (SLEEP_MODE_PWR_DOWN);  
    power_adc_disable();
    power_spi_disable();
    power_timer0_disable();
    power_timer1_disable();
    power_timer2_disable();
    power_twi_disable();

    sleep_enable();
    digitalWrite (AWAKE_LED, LOW);
    interrupts ();
    sleep_cpu ();      
    digitalWrite (AWAKE_LED, HIGH);
    sleep_disable();
    power_all_enable();

    ADCSRA = old_ADCSRA;
  }  // end of time to sleep

  if (mySerial.available () > 0)
  {
    byte flashes = mySerial.read () - '0';
    if (flashes > 0 && flashes < 10)
      {
      // flash LED x times 
      for (byte i = 0; i < flashes; i++)
        {
        digitalWrite (GREEN_LED, HIGH);
        delay (200);  
        digitalWrite (GREEN_LED, LOW);
        delay (200);  
        }
      }        
  }  // end of if

}  // end of loop

睡眠时的功耗为260 nA(0.260 µA),因此在不需要时的功耗非常低。

请注意,通过这样设置保险丝,处理器以8 MHz运行。因此,您需要告知IDE(例如,选择“ Lilypad”作为板类型)。这样,延迟和SoftwareSerial将以正确的速度工作。


@NickGammon非常感谢!我已经做到了,它奏效了。这种方式在我们日常使用的其他产品中是否很常见,或者它们还有其他方式来收听通讯和睡眠?(所有MCU都无法在深度睡眠时听uart吗?)
Curnelious

我正在阅读数据表,它指出,使用内部振荡器时,只要使用BOD,就只需14个时钟周期即可启动芯片。如果电源始终处于打开状态(电池),是否可以在没有BOD的情况下使用?当然违反了规格。这将在进入UART边沿后很短的时间内使芯片启动,但是我仍然不确定是否足以捕获第一个字节。
FarO

是的,14个时钟周期并不长,但是UART仍可能会错过边沿(毕竟,边沿是处理器注意到更改的时间)。因此,即使它在边缘之后很快启动,也可能会错过它。
尼克·加蒙

进行一些测试表明(即使启用了BOD)它也不起作用。处理器需要醒来才能注意到上升沿(起始位),因此在接收到上升沿后将其加电(即使之后不久)也无法正常工作。
尼克·加蒙

复位后有14个时钟周期。如果使用内部RC振荡器,则掉电后仅需要6个周期。请参阅其他示例代码。
尼克·加蒙
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.