Blogs

微控制器简介 - 更多的定时器和显示器

Mike Silva.2013年10月15日3评论

快速链接

在定时器周围建立世界

到目前为止,您已经看到了四种不同的方法来使用程序中的计时器。 接下来,我们将在定时器的帮助下看一些方法来产生程序中的多个并行工作流的效果。 这种效果只是一种外观,而不是现实,因为单个微控制器(一个核心)只能运行单线的代码线程。 然而,由于微控制器与应用的许多任务相比如此迅速,因此我们可以使用这种速度来实现并行线程或任务的效果。 一些常见的并行任务可能需要典型的嵌入式系统:

  • 检查用户输入
  • 更新显示
  • 执行一个或多个状态机
  • 阅读新的ADC值
  • 生成新的PWM输出
  • 发送和接收消息

对于所有此类任务需要满足于,平均完成任务的一个迭代的时间小于任务必须运行的时间。 有时这不是一个问题 - 我们可以每10ms和我们的用户输入代码检查一次用户输入,可能只需要20us到运行20us,或者用户输入任务期的1/500。 对其他代码进行充足的时间留下了足够的时间。 另一方面,如果任务平均运行到运行的时间超过它必须运行的时间,那么您要么需要缩短任务(更好的算法,更快的硬件)或增加时段(例如,仅计算控制循环每20ms而不是每10ms)。 当然,这同样适用于所有任务的总时间 - 如果总和总数可防止任何任务以其分配的速率运行,则必须加速系统或以其他方式重新设计。 

一个循环来统治它们

对于嵌入式解决方案的简单但经常有效的结构是循环+中断结构,有时称为“循环执行”,因为它围绕执行各种任务的循环循环,因为它们需要它们。 我们已经在我们的一些中断驱动的程序中看到了这种结构。 主循环连续执行,每次都会通过它测试各种 确定接下来要做什么的事件。 这些事件通常是时间截止日期的到期或由于中断或其他代码执行而导致标志的设置。 只要循环可以经常重复,使得每个任务都足够及时地注意力, 这将是一个合适的解决方案。

循环选择根据其检查每次通过的任何标志或数据进行下一步。 事实上,我们甚至可以通过在循环顶部的优先级任务中施加测试和代码来对我们的任务产生一种优先级。 如果我们编写循环(甚至只是循环的一部分) 以便最多只做 每个循环的一个任务动作,循环顶部的任务(或部分) 如果它们的运行条件为真,请始终在运行时首先拍摄。 我们稍后会展示一个例子。

时间为你,不适合我

有几种方法可以基于单个计时器的操作以不同的间隔以不同的间隔运行。 这是一种非常灵活的方法。 计时器生成正在进行的勾号,定时器勾号ISR不断更新内存中的时间值。 每个任务都被分配了一个截止日期,之后它必须运行,并且每次通过循环,每个任务都会比较当前时间值(定时器刻度ISR的值,以截止日期为止。 如果当前时间值在截止日期后,则传递截止日期并执行任务代码。 作为任务运行的一部分,它通常会在下次运行时分配新的截止日期。

对于我们的示例,我们将在每2毫秒滴定(并更新16位毫秒值)的定时器(并更新16位毫秒值)(没有关于此数字的魔法,这只是一个很好的例子)。 这将使我们的时间间隔为32秒(为什么不65+? - 你会在一点中看到)。 能够在32秒内完成时间,具有2ms的精度,对于大量典型的嵌入式任务将是有用的,但这些数字未设置为石头,肯定是 想出自己的。 对于更长的间隔,您可以始终保留每1000ms更新的运行秒计数器,或者您可以使用32位运行的时间变量,这使得截止日期在8位设备上检查一点计算昂贵,但可让您花时间超过几百万秒。 当然,在32位设备上,这都不是一个问题 - 32位架构甚至没有问题,这是一个甚至没有问题,这是他们有利的重要因素之一。

首先说明我们的2MS定时器刻度ISR是多么简单:

ISR(TIMER1_COMPA_vect)
{
  MS_value += 2;
}

由于我们的计时器勾选每2ms,因此我们只需添加2即可更新我们的运行时间值(MS_Value)。  拥有我们的定时器值代表毫秒,而不是一些任意刻度时间是拍摄可读性和可编程性的特许权,但是,无论定时器滴答间隔如何,人才可以轻松增量时间值并将其称为滴答数。 就个人而言,我更喜欢根据微秒和毫秒而不是秒来思考。 请注意,我们允许我们的运行时间值完全滚动,因为我们允许跳过计时器滚动,并且出于同样的原因。

我们现在需要的是一个止血比较功能。 鉴于截止日期,我们想知道运行时间是否已达到或过去这一截止日期。 我们不能只检查运行时间是否等于截止日期,因为我们可能不会很快检查截止日期,因为其他任务代码已经运行。 那是,如果是。我们正在检查2000ms的截止日期,我们可能无法检查截止日期,直到MS_Value先进到2002年或2004年。 我们需要的是能够知道运行时间是否等于或之后截止日期(不硬),并且即使运行时间已经过去(更难),我们也需要计算。 一个简单的例子显示了问题。 假设当前的运行时间为65,000ms,截止日期已设置为1536ms,或(当前时间翻转后),1000ms。  所需要的是一个计算,以识别1000的“更大”,即在时间上的时间,而不是65,000,以及65,000和(1000-1)之间的所有计时器值。 2S补充数学的魔力为我们提供了一个简单的解决方案。 这是功能:

u8 deadline_reached(u16 dl)
{
  return ((int16_t)(MS_value - dl) > 0) ? 1 : 0;
}

通过将MS_VALUE - DL(当前时间 - 截止日期)的结果转换为符号值,我们可以检查当前时间是否在截止日期之前或之后只是通过查看结果的标志。 无论MS_VALUE或DL的无符号大小如何,它都有一个限制,其中没有截止日期时间可以设置超过2 ^ 15-1或32767,远离当前时间(对于16位时间值)。  这就是为什么我们只能使用16位时间值延迟到32秒以上,而不是超过65秒,但这是为实现的实用程序支付的非常小的价格。 当然,如果您愿意,该函数可以加入或变成宏,以提高截止日期检查的速度。

我们的截留()函数只有一个问题 - 在一个8位设备上,它被打破了。 这是旧中断数据损坏的hobgoblin再次。 如果定时器勾选ISR更改MS_Value,则在截止日期_REACHED()函数访问它以执行减法时? Mayhem!  因此,对于8位设备,我们的功能需要防止这种可能性,例如:

u8 deadline_reached(u16 dl)
{
  cli();
  u16 temp_ms_value = MS_value;
  sei();
  return ((int16_t)(temp_ms_value - dl) > 0) ? 1 : 0;
}

现在MS_Value无法在访问它时更改,这是避免数据损坏所需的内容。

要生成新的截止日期,我们只需将所需的时间间隔添加到当前时间,如下所示:

u16 make_deadline(u16 t)
{
  return (MS_value + t);
}

要生成立即运行某些任务代码,我们可以使用make_deadline(0)将其截止日期设置为当前时间 - 下次通过循环时,该截止日期将被识别,并且任务将执行。 要创建定期截止日期,我们只需将时间间隔添加到当前截止日期以获得下一个截止日期 - 再次与硬件定时器的跨越相似。 由于我们在这里所做的一切都在延伸到混合硬件 - 软件定时器的所有情况,因此这种相似度不应该令人惊讶。

请注意,make_deadline()返回的截止日期偏置到短边。 这是因为MS_Value可以在创建截止日期后随时前进。 特别是,make_deadline(1)可能会返回立即达到的截止日期,因为在执行添加后MS_Value刚刚高级。 通常,Make_Deadline(n)返回的真实截止日期将来在N-1和N MS之间。 这不是由于任何错误,而是仅仅是一个计时器计数水平在一个定时器计数(在这种情况下的一个MS_VALUE计数)的结果。 定时器的任何异步访问(与定时器操作不同步的访问)将运行到同一问题中。 如果截止日期短边偏见是一个问题,只需将make_deadline调用t + 1而不是t。

使用计时器进行延迟

如果我们的当前时间缩放为毫秒,则这是一种创建MS_Delay()函数的另一种方法,以及不依赖于魔术号码或UC时钟速率的方法。 嗯,依赖于时钟速率,但是通过计时器的计算将得到处理,以便完全每n毫秒生成刻度。 无论如何,这里是基于SIMPLE的TIMER-adelay():

void ms_delay(u16 d)
{
  u16 dl = make_deadline(d+1);
  while (!deadline_reached(dl))
    ;
}

我们只是根据延迟时间设置截止日期,然后等待达到该截止日期。 我们在截止日期计算使用D + 1的原因是避免上面讨论的短边偏差。 也就是说,通过使用make_deadline(d + 1),延迟将是d<= delay <= d+1.  不使用d + 1,延迟将是d-1<= delay <= d.

32位计时器 - 一个完整的世界

我们现在正在使用的部分都没有32位计时器,但许多其他家庭都这样做,如恩智浦家族。 32位计时器让你e.g.假设您可以为传入的预乘时钟选择适当的预析标值来测量微秒至2000秒(超过30分钟)。 查看此方法是,而不是16位硬件计时器,后跟16位软件毫秒计数器,现在可以在32位的硬件定时器中完成一切。 使用微秒分辨率时钟,您还可以使用等待截止日期方法测量较小的时间间隔,例如如果不使用忙标志,则LCD显示所需的50us延迟。

重新审视LCD显示屏

此时,我们终于查看了LCD代码的STM32版本,具有来自AVR版本的一些更改。

//
// lcd.c
//

#include <stm32f10x.h>
#include "lcd.h"

extern void set_multiple_GPIO(GPIO_TypeDef * p_port, int first_bit, u32 val, int num_bits);
extern void set_GPIO(GPIO_TypeDef * p_port, int bit, u32 val);

#define LCD_USE_BF

void lcd_strobe(void)
{
  LCD_PORT->BSRR = LCD_E;
  nano_delay();       //tiny_delay(LCD_STROBE);
  LCD_PORT->BRR = LCD_E;  // must be >= 230ns
}

void LCD_PORT_data(u8 d)
{
  LCD_PORT->ODR = (LCD_PORT->ODR & ~LCD_DATA) | (d & 0xf0);
}

#ifdef LCD_USE_BF
void lcd_wait(void)
{
  u8 data;

  u32 d = GPIOC->CRL & 0x0000FFFF;          // strip out PC4-PC7
  GPIOC->CRL = d | 0b0100<<16 | 0b0100<<20 | 0b0100<<24 | 0b0100<<28;  // inputs

  LCD_PORT->BSRR = LCD_RW | (LCD_RS<<16);   // set RW, clear RS (read, cmd)
  
  do 
  {
    LCD_PORT->BSRR = LCD_E;                 // 1st nybble, read BF
    data = LCD_PORT->IDR;
    LCD_PORT->BRR = LCD_E;
    nano_delay();       //tiny_delay(LCD_STROBE);
    LCD_PORT->BSRR = LCD_E;                 // 2nd nybble, don't read data
    nano_delay();       //tiny_delay(LCD_STROBE);
    LCD_PORT->BRR = LCD_E;
    nano_delay();       //tiny_delay(LCD_STROBE);
  } while (data & LCD_BF); 

  LCD_PORT->BRR = LCD_RW;                   // set to write
  d = GPIOC->CRL & 0x0000FFFF;              // strip out PC4-PC7
  GPIOC->CRL = d | 0b0010<<16 | 0b0010<<20 | 0b0010<<24 | 0b0010<<28;  // outputs
}
#endif

void lcd_send_cmd(u8 cmd)
{
#ifdef LCD_USE_BF
  lcd_wait();
#endif
  LCD_PORT->BRR = LCD_RS;           // cmd
  LCD_PORT_data(cmd);               // write hi 4 bits
  lcd_strobe();
  LCD_PORT_data(cmd << 4);          // write lo 4 bits
  lcd_strobe();
#ifndef LCD_USE_BF
  tiny_delay(90);
#endif
}

void lcd_putc(u8 c)
{
#ifdef LCD_USE_BF
  lcd_wait();
#endif
  LCD_PORT->BSRR = LCD_RS;          // data
  LCD_PORT_data(c);                 // write hi 4 bits
  lcd_strobe();
  LCD_PORT_data(c << 4);            // write lo 4 bits
  lcd_strobe();
#ifndef LCD_USE_BF
  tiny_delay(90);
#endif
}

void lcd_init(void)
{
  set_multiple_GPIO(GPIOC, 1, 0b0010, 7); // PC1-PC7 set to outputs
  GPIOC->BRR = LCD_RW | LCD_RS | LCD_E; // write, cmd, no E pulse

  set_GPIO(GPIOD, 2, 0b0010);   // backlight to output

  ms_delay(15);                 // delay after powerup
  LCD_PORT_data(DISP_INIT);
  lcd_strobe();                 // pseudo 8-bit command
  ms_delay(5);
  LCD_PORT_data(DISP_INIT);
  lcd_strobe();                 // pseudo 8-bit command
  ms_delay(1);
  LCD_PORT_data(DISP_INIT);
  lcd_strobe();                 // pseudo 8-bit command
  ms_delay(1);

  LCD_PORT_data(DISP_4BITS);
  lcd_strobe();                 // pseudo 8-bit command
  ms_delay(1);

  lcd_send_cmd(DISP_CONFIG);
  lcd_send_cmd(DISP_OFF);
  lcd_send_cmd(DISP_CLR);
  ms_delay(2);  // undocumented delay required for Clear display command
  lcd_send_cmd(DISP_EMS);
  lcd_send_cmd(DISP_ON);
  lcd_backlight(1);
}

void lcd_clear(void)
{
  lcd_send_cmd(DISP_CLR);
  ms_delay(2);  // undocumented delay required for Clear display command
}

void lcd_display(int x, int y, const char *str)
{
  int n = LCD_WIDTH - x;
  u8 addr;

  if ((y < 0) || (y >= LCD_LINES))
    return;

  switch (y)
  {
  default:
  case 0:
    addr = DD_RAM_ADDR;
    break;
  case 1:
    addr = DD_RAM_ADDR2;
    break;
  case 2:
    addr = DD_RAM_ADDR3;
    break;
  case 3:
    addr = DD_RAM_ADDR4;
    break;
  }
  lcd_send_cmd(addr + x + 0x80);
  while (*str && n--)
    lcd_putc(*str++);
}

void lcd_backlight(u8 on)
{
  if (on)
      GPIOD->BSRR = LCD_BL;      // turn on backlite
  else
      GPIOD->BRR = LCD_BL;      // turn off backlite
}

在STM32的变化中,其中一个是使用原子GPIO位集合&重置寄存器以控制显示RS,RW和E行。 你甚至可以在一个案例中看到BSRR寄存器用于在同一指令中设置RW HI和RS LO - 方便!

另一个变化是使用定时器驱动的MS_Delay()函数,例如我们上面讨论的。 这与1ms定时器勾号相结合,其中递增MS_Value每毫秒,所以现在,除Nano_delay()除外,我们的代码中没有软件延迟。 这是99例中有件好事。

若有更改是函数set_gpio(),这使得配置GPIO引脚更容易。 另请注意,我不使用此函数的一个地方在LCD_WAIT()中,必须将所有4个数据线更改为输入,然后返回输出。 在这里,我只需执行相关的CFL寄存器的单个访问,以便速度,而不是4个函数调用。

一个惊喜的gotcha! (或者,为什么你需要一个范围#37)

在使用此代码时,我正在测试不同优化设置对代码速度的影响。 从无优化切换到O1优化的差异约为3x加速 - 令人印象深刻! 从O1转到O2切换变得更大 - 它违反了代码。 突然间,似乎是毫秒的中断运行两倍! 困惑,我在毫秒中断内切换了一个LED,因此每次MS_Value都递增,LED被切换。 然后我把范围放在LED上。 由于中断发生了每毫秒,我期望看到一个带有2ms的方波,但这并不是我所看到的。 相反,我看到了每毫秒的一次非常短的脉冲。 这意味着每毫秒,ISR在快速成功中运行两次(因此MS_Value每毫秒递增两次而不是一次,从而打破所有系统时序)! 就好像在ISR中的最后一行中清除的中断标志没有足够快地清除,因此在返回的ISR后立即再次触发中断。 我搬了线条清除旗帜到ISR的开始,并确定问题走了。 对互联网的一点研究表明,这是一个已知的问题,与控制GPIO部分定时的CPU时钟和外围时钟之间的速度差异有关。 通过较低的优化设置,编译器正在生成足够的附加代码,以便在返回的ISR之前设置标志,但在更高的优化设置之前,将删除此额外代码,并浮出水面。 您现在将遇到如此奇怪的优化相关的故障,然后,这只是游戏的一部分。 因此,STM32的新规则 - 始终清除ISR开头或附近的中断标志。

然后我走到并改为Systick计时器,这不需要清除ISR中的中断标志。 不是因为它更容易,而是引入Systick计时器,这是用于生成系统勾号的专用计时器。

利用8位计时器

如果您使用的是具有8位和16位计时器的AVR或其他UC,您会发现您经常想要保存更强大的16位计时器以进行PWM或输入捕获或更复杂的时序任务。 如果可能,那么,您希望为系统勾号使用8位计时器,以便为这些其他任务留下16位计时器。 问题是,通过AVR的定期值选择,没有大的选择是1Hz的精确整数倍数。 例如,使用8位计时器无法使用8MHz或16MHz时钟进行精确的10ms滴定。 但是,我们可以更短的刻度为1或2ms,这是一个适用于运行时间的调度方法。

不是每项任务都是时间驱动的

我们现在有一种简单的技术来在简单的循环执行中执行时间触发的任务。 每次通过循环都会检查截止日期,直到找到已达到的截止日期,然后我们执行该截止日期的代码并计算下一个截止日期。 但并非所有任务都是时间驱动的。 许多任务必须基于外部事件执行,这些事件不是时间的,但每当他们来时都会来。 示例可能是到达Comms通道的消息,或来自某些附加设备的控制信号。

幸运的是,我们的周期性执行官可以处理此类事件驱动的任务,没有问题。 通常,这些事件将具有相关中断,并且在事件ISR内部将设置事件标志以触发主循环中的进一步处理。 在循环中,检查事件标志是否可以易于检查截止日期。

循环执行框架

这是循环执行的框架的一个例子。 请记住,定时器滴答中断运行在同时运行,连续更新当前时间值对比所有截止日期值进行比较。

while (1)
{
  if (deadline_reached(DL1))
  {
    task1();
    DL1 += SOME_CONSTANT_1;
  }
  if (deadline_reached(DL2))
  {
    task2();
    DL2 += some_calculated_value();
  }
  if (flag3)
  {
    task3();
    flag3 = 0;
  }

  if (flag4)
  {
    task4();
    flag4 = 0;
  }
  else if (deadline_reached(DL5))
  {
    task5();
    DL5 += SOME_CONSTANT_5;
  }
  else if (flag6 || deadline_reached(DL6))
  {
    task6();
    flag6 = 0;
    DL6 += SOME_CONSTANT_6;
  }
}

这里有几件事可以注意到。

  • Task1显示了具有固定时段的时间驱动任务。
  • Task2显示具有可变时段的时间驱动任务。
  • Task3显示一个标志驱动(事件驱动)任务。

注意在这种情况下,任务1,任务2和任务3中的每一个都会有机会通过循环运行。 这适用于更高优先级的任务。

  • Task4显示另一个标志驱动的任务。
  • Task5以固定时段显示另一个时间驱动任务。
  • Task6显示了一个是时间驱动的任务以及旗帜驱动。

注意在这种情况下,只有一个任务4,任务5和任务6可以每次都会通过循环运行。 这适用于较低优先级任务,尤其是可能具有更长的运行时间的任务。  只有允许一组任务中的一个通过循环运行,它确保循环顶部的较高优先级任务将有机会以更及时的方式运行。

任务6是一个有趣的例子,因为它至少经常运行它的时间,但它也可以 被触发立即运行。 您可能会看到此行为的一个地方是使用显示任务。 任务可能每400-500ms运行,以显示更新值(时间,温度等),但如果用户按下键,或者如果必须在没有延迟的情况下向用户显示某些其他重要事件,则它也可以立即触发以立即运行。 。

一个简单的例子

这是一个简单的例子,这次为STM32,使用循环管理维护两个计数器,一个100ms计数器和1秒计数器,一个计数器值的LCD显示,以及两个按钮,一个重置两个计数器。 任务将如下:

  • 100ms反时驱动,100ms
  • 1秒反时驱动,1000ms
  • LCD显示 - 时间和事件驱动,400ms
  • 按钮检查 - 时间驱动,50ms

只是有所不同,我们将使用1ms定时器刻度而不是上面讨论的2ms勾选,我们将使用ARM Cortex Systick计时器进行勾号 - 这就是它在那里的东西,它实际上比其他定时器更容易使用。 400ms LCD显示更新期基于更新显示器之间的平衡,通常足以快速反映系统状态,而不会如此快速更新,即更改数据变为混交框。 每个显示形势都是唯一的,并呼叫关于显示更新速率的特定决策。 例如,不同的更新速率可用于不同的显示行,或用于不同的操作模式。 选择50ms按钮检查周期以便快速响应任何按钮推动。

//
// STM32_LCD2
// 1ms tick
//

#include <stm32f10x.h>
#include <stdio.h>
#include "defs.h"
#include "lcd.h"

#define F_CPU       8000000UL	// 1MHz
#define PRESCALE    8
#define PERIOD      1	// milliseconds
#define TCLKS       ((F_CPU/PRESCALE*PERIOD)/1000)

void set_GPIO(GPIO_TypeDef * p_port, int bit, u32 val)
{
  if (bit < 8)
  {
    u32 port_data = p_port->CRL & ~(0b1111 << (4*bit));   // clear out config for this bit
    p_port->CRL = port_data | (val << (4*bit));           // add in new config for this bit
  }
  else  // hi bits
  {
    bit -= 8;
    u32 port_data = p_port->CRH & ~(0b1111 << (4*bit));   // clear out config for this bit
    p_port->CRH = port_data | (val << (4*bit));           // add in new config for this bit
  }
}

void set_multiple_GPIO(GPIO_TypeDef * p_port, int first_bit, u32 val, int num_bits)
{
  while (num_bits-- != 0)
  {
    set_GPIO(p_port, first_bit, val);
    first_bit++;
  }
}

int main(void)
{

  RCC->CFGR = 0;                      // HSI, 8 MHz, 

  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // enable PORTA
  set_GPIO(GPIOA,  8, 0b0010);        // CNF=0, MODE=2 (2MHz output) PA8 LED
  set_GPIO(GPIOA, 11, 0b1000);        // button inputs, pulldowns
  set_GPIO(GPIOA, 12, 0b1000);

  RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; // enable PORTB
  set_GPIO(GPIOB, 5, 0b0010);         // CNF=0, MODE=2 (2MHz output) PB5 LED

  RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // enable PORTC
  set_GPIO(GPIOC,  8, 0b0010);        // CNF=0, MODE=2 (2MHz output) PC8,PC9 LEDs
  set_GPIO(GPIOC,  9, 0b0010);
  set_GPIO(GPIOC, 10, 0b0010);        // row output for reading buttons
  GPIOC->ODR |= (1<<10);              // 1 when button pushed

  RCC->APB2ENR |= RCC_APB2ENR_IOPDEN; // enable PORTD

  RCC->APB2ENR |= RCC_APB2ENR_TIM1EN; // enable Timer1

  SysTick->LOAD = F_CPU/1000-1;       // 1ms tick (-1 rule!)
  SysTick->CTRL = 0b111;              // internal clock, enable timer and interrupt

  lcd_init();
  lcd_display(0, 0, "  Cntr1:");      // only need to write these once
  lcd_display(0, 1, "Cntr100:");

  u16 cntr1_dl = make_deadline(0);
  u16 cntr100_dl = make_deadline(0);
  u16 button_dl = make_deadline(0);
  u16 LCD_dl = make_deadline(0);
  u8  LCD_flag = 0;

  u16 cntr1 = 0;
  u16 cntr100 = 0;

  u8 old_b1 = 0;
  u8 old_b100 = 0;

  while (1)
  {
    if (deadline_reached(cntr1_dl))
    {
      GPIOC->ODR ^= (1<<9);  // LED
      cntr1++;
      cntr1_dl += 1000;
   }
    if (deadline_reached(cntr100_dl))
    {
      GPIOC->ODR ^= (1<<8);  // LED
      cntr100++;
      cntr100_dl += 100;
    }
    if (deadline_reached(button_dl))
    {
      u8 new_b = (GPIOA->IDR & (1<<11)) ? 1 : 0;
      if (new_b && !old_b1)
      {
        cntr1 = 0;
        LCD_flag = 1;
        LCD_dl = make_deadline(400);
      }
      old_b1 = new_b;

      new_b = (GPIOA->IDR & (1<<12)) ? 1 : 0;
      if (new_b && !old_b100)
      {
        cntr100 = 0;
        LCD_flag = 1;
        LCD_dl = make_deadline(400);
      }
      old_b100 = new_b;

      GPIOB->ODR ^= (1<<5);  // LED
      button_dl += 50;
    }
    if (LCD_flag || deadline_reached(LCD_dl))
    {
      char buf[LCD_WIDTH+1];

      sprintf(buf, "%5u", cntr1);
      lcd_display(9, 0, buf);
      sprintf(buf, "%5u", cntr100);
      lcd_display(9, 1, buf);

      GPIOA->ODR ^= (1<<8);  // LED

      if (LCD_flag)
      {
        LCD_flag = 0;
        LCD_dl = make_deadline(400);
      }
      else
        LCD_dl += 400;
    }
  }
}

请注意,我们寻找两个不同按钮的前沿(new_b&&当按钮刚刚从0到1)时,Old_B仅为True,每个按钮清除两个计数器中的一个。 任何时候计数器清除,我们还将LCD_FLAG设置为强制立即显示更新,并重新计算计数器的新全长截止日期。良好的用户界面设计要求使用某种形式的视觉或可听反馈遵循按钮或键按钮 - 此处 this feedback 是立即显示的清除计数器值。 还要注意必须配置4x4按钮矩阵(我们尚未谈论的))如何配置 to enable the two buttons. PC10设置顶部按钮行高,PA11 / PA12设置为带有下拉的输入,因此它们将保持低(由于下拉) 除非推出前两个最右侧的按钮之一,否则输入的输入将会高。 在发布下一个教程章节后,这将变得更加清晰,这将处理按钮矩阵。 现在,它足以知道PA11和PA12被设置为保持低电平的输入 their associated buttons 被推动,此输入变高。

另请注意,每个任务在活动时,会切换不同的LED。 这只是为了给出任务活动时的视觉提示。

最后,观察到1秒钟的数量不是光滑的,但似乎持续长短短路...... 这是400ms显示更新速率的伪影,这不是1秒的uplultiple(500ms或250ms是倍增的)。 这导致为2个LCD更新(800ms)显示的计数的一个值以及显示3 LCD更新(1200ms)的下一个值。  If something like 这是一个问题,可以选择一个问题 LCD更新速率是一个问题的泛滥,或者您可以每次1秒计数器增量时立即更新设置LCD_FLAG。 我建议您尝试两种变体,以查看行为的差异。

以下是示例程序的短视频在操作:

下一页

我们将看看这个神秘的4x4按钮矩阵以及如何阅读它,并看看一些其他硬件。


[]
评论 马丁48.2014年10月7日
您还可以使用重新使用^ aold来查看哪些位已更改。然后测试一个前沿和否则拼写的重复测试。另外,为什么不使用宏为timeout_expired?
[]
评论 Paul_Knieriem.2020年12月15日

在您的计时器中不会加起来。


让我们说你有一个16位ms_value = 65498

和具有截止日期 - 时间(DL)= 65500的特定计时器,

然后截留函数计算:

(MS_VALUE - DL)=(65498 - 65000)= -2(负2),

这不是>0,所以它返回“0”。


勾选ISR递增MS_VALUE + = 2 - > 65500.

(ms_value - dl)=(65500 - 65500)= 0,

这不是>0,所以它返回“0”。

但此时它真的应该被触发。


勾选ISR递增MS_VALUE + = 2 - > 65502.

(ms_value - dl)=(65502 - 65500)= 2(正面2),

这是>0,所以它返回“1”。

接下来,触发的例程重新加载其常量值:

截止日期(DL)= 65500 + 500(const)= 66000

-->16位滚动= 66000-65535 = 465。


下一个蜱虫:

ISR递增MS_VALUE + = 2 - > 65504.

(MS_VALUE - DL)=(65504 - 465)= 65039(正),

这是>0,所以它立即返回一个“1”。

[]
评论 mjsilva.2020年12月15日

嗨保罗,

指某东西的用途>0 vs >= 0只会向测试添加一个或另一个方式。任何一个都是正确的。我用>0避免“快速火灾”情况:

- timer刻度tt为0

- 码集目标刻度为1(通过计算TT + 1)

- Timer Tick滚动到1

假设这一切都发生在几微秒内。现在测试>= 0将立即射击,导致非常短的延迟(远低于整个刻度期)。等待将在略高于0的任何地方,最多1 TT计算测试>0将强制等待至少1 TT计数,最多2 TT计数。这只是一个测试对您感到的事项,具体取决于你是否想要偏见<所需的刻度计数,或>所需的刻度。在任何情况下,偏差将是长度的一小部分,并且是不可避免的。

在溢出数学问题上,您已确定必须将(MS_Value - DL)的结果评估为签名编号。作为签名的号码,65039被解释为-497,而-497则不是>0 (or >= 0)。随着MS_Value的增加,符号差异将从-497到-1移动,然后0,+1等。

要发布回复评论,请单击连接到每个注释的“回复”按钮。发布新的评论(不是回复评论),请在评论的顶部查看“写评论”选项卡。

注册将允许您参加所有相关网站的论坛,并为您提供所有PDF下载。

注册

我同意 使用条款隐私政策.

尝试我们偶尔但流行的时事通讯。非常容易取消订阅。
或登录