65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Arduino Due 上获取 USB 网络摄像头视频流 - 第三部分:看门狗定时器和 TFT 显示器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (6投票s)

2015 年 3 月 30 日

CPOL

7分钟阅读

viewsIcon

24901

downloadIcon

383

看门狗定时器和 TFT 显示器

引言

第 2 部分在此

本文主要介绍TFT显示器。我将对其进行初始化,创建必要的函数来输出像素信息并编写一些测试代码。但在开始之前,让我们运行上一篇文章中的示例并观察其RS-232显示器程序的输出一段时间。

看门狗定时器

到目前为止,示例代码执行以下操作

1. 首先它运行Print函数的测试代码(PrintDEC、PrintBIN和PrintHEX)——这来自第2部分。

2. 然后每秒输出“Hello world”——这来自第1部分。

让我们观察输出

如您所见,测试函数不知何故再次执行,这只能说明一件事——Arduino Due 重新启动。如果您长时间运行示例代码,您会发现只要 Arduino Due 开机,重启就会周期性地发生。

这种行为并非错误,而是看门狗定时器**[1, p.267]**引起的一个特性。有人可能会问,我们为什么需要这个特性?原因有很多,但想象一下,如果您的医疗设备由支持患者生命的微控制器控制,它就不能停止,因为停止会导致患者死亡。而且,与所有数字设备一样,在某些不可预见的情况下,它可能会挂起。这时重启就很有用了。

看门狗在倒计时归零后重启系统。为了防止重启,程序应该要么完全关闭看门狗,要么定期(在看门狗计数归零之前)重新加载看门狗定时器值。

不需要初始化代码,因为看门狗默认工作,并会在16秒内重启Arduino Due。我不会将其关闭,我宁愿每秒重新加载其值,并且我们已经在代码中有一个切换LED灯的位置。

void SysTick_Handler(void)
{
    uint32_t Pin_LEDRX;
    uint32_t Pin_LEDTX;
    
    SysTickCounter ++;
    if(1000 == SysTickCounter)
    {
        SysTickCounter = 0;
        
        Pin_LEDRX = 1 << (PIO_PC30_IDX & 0x1Fu);
        Pin_LEDTX = 1 << (PIO_PA21_IDX & 0x1Fu);
        if(PIOC->PIO_PDSR & Pin_LEDRX)
        {
            PIOC->PIO_CODR = Pin_LEDRX;    //Lights on
            PIOA->PIO_CODR = Pin_LEDTX;
        }
        else
        {
            PIOC->PIO_SODR = Pin_LEDRX;    //Lights off
            PIOA->PIO_SODR = Pin_LEDTX;
        }
        
        WDT->WDT_CR = WDT->WDT_CR | WDT_CR_KEY_PASSWD | WDT_CR_WDRSTT;    //Re-load watchdog
        PrintStr("Hello World\r\n");
    }
}

为了重新加载看门狗定时器值,WDRSTT位被写入控制寄存器 (WDT_CR) [1, p.268]。构建项目,上传到 Arduino Due,并观察到不再发生重启。

TFT 显示器

正如您在第 1 部分中读到的,我的 TFT 监视器是一个分辨率为 320x240 的屏蔽板。它基于 SSD1289 控制器。因此,如果您真的想学习如何初始化它,您可以尝试从 SSD1289 文档中理解其初始化序列。我宁愿专注于像素输出函数,为此我将采用某个 8 位 AVR 论坛上的代码并将其修改为与 32 位系统兼容。拥有屏蔽板的引脚映射以查看与 Arduino Due 引脚的对应关系也很有用。

从图中可以看出,数据引脚分散在所有四个PIO(并行输入/输出控制器**[1, p.641]**)上,这意味着要输出16位像素数据,必须执行多次移位和输出操作。这很糟糕,因为在实时USB操作中,如果能一次性(而不是多次移位和输出操作)输出数据就太好了。

让我们将SSD1289.h和SSD1289.c文件添加到项目中。添加

#include "SSD1289.h"

到 SSD1289.c 中,并添加到主文件。然后打开 SSD1289.h,我们在其中放置一些宏。首先是用于命令引脚的宏

#define LCD_CLR_CS()            PIOC->PIO_CODR = PIO_PC8
#define LCD_SET_CS()            PIOC->PIO_SODR = PIO_PC8

#define LCD_CLR_RS()            PIOC->PIO_CODR = PIO_PC6
#define LCD_SET_RS()            PIOC->PIO_SODR = PIO_PC6

#define LCD_CLR_WR()            PIOC->PIO_CODR = PIO_PC7
#define LCD_SET_WR()            PIOC->PIO_SODR = PIO_PC7

#define LCD_CLR_RST()           PIOC->PIO_CODR = PIO_PC9
#define LCD_SET_RST()           PIOC->PIO_SODR = PIO_PC9

CS表示芯片选择,只有当该引脚为低电平时,我们才能与显示器通信。

RS 定义显示器所处的模式,是数据模式还是命令模式。因此,我们可以发送数据或命令。

WR 激活命令或数据传输到显示器。

RST 表示复位。

向显示器输出数据或命令的顺序如下:在显示器引脚上设置16位数据或命令(取决于RS引脚),然后必须清除WR并再次设置。

接下来是用于16位数据或命令输出的宏

#define LCD_SET_DB(x)  PIOA->PIO_ODSR =  ((x & PIO_PA6) << 1)\
                                       | ((x & (PIO_PA9 | PIO_PA10)) << 5);\
                       PIOB->PIO_ODSR =  ((x & PIO_PB8) << 18);\
                       PIOC->PIO_ODSR =  ((x & PIO_PC4) >> 3)\
                                       | ((x & PIO_PC3) >> 1)\
                                       | ((x & PIO_PC2) << 1)\
                                       | ((x & PIO_PC1) << 3)\
                                       | ((x & PIO_PC0) << 5);\
                       PIOD->PIO_ODSR =  ((x & (PIO_PA11 | PIO_PA12 | PIO_PA13 | PIO_PA14)) >> 11)\
                                       | ((x & PIO_PA15) >> 9)\
                                       | ((x & PIO_PD7) << 2) \
                                       | ((x & PIO_PD5) << 5);\

如您所见,有4个输出操作,12个移位操作和一些按位或操作。速度不快。

我已经提到输出分散在所有四个PIO上,因此为了使上述宏工作,必须以与我们在第一部分中对LED引脚相同的方式初始化相应的引脚。让我们快速回到system_sam3xa.c文件(参见第一部分),并在LED引脚初始化之后和SysTick初始化之前插入以下初始化代码。

/* TFT connection pins initialization */
uint32_t ul_pin_pos = PIO_PA7 | PIO_PA14 | PIO_PA15;
PIOA->PIO_IDR = ul_pin_pos;
PIOA->PIO_MDDR = ul_pin_pos;
PIOA->PIO_SODR = ul_pin_pos;
PIOA->PIO_OER = ul_pin_pos;
PIOA->PIO_PER = ul_pin_pos;
        
ul_pin_pos = PIO_PB26;
PIOB->PIO_IDR = ul_pin_pos;
PIOB->PIO_MDDR = ul_pin_pos;
PIOB->PIO_SODR = ul_pin_pos;
PIOB->PIO_OER = ul_pin_pos;
PIOB->PIO_PER = ul_pin_pos;
       
ul_pin_pos = PIO_PC1 | PIO_PC2 | PIO_PC3 | PIO_PC4 | PIO_PC5 | PIO_PC6 | PIO_PC7 | PIO_PC8 | PIO_PC9;
PIOC->PIO_IDR = ul_pin_pos;    
PIOC->PIO_MDDR = ul_pin_pos;
PIOC->PIO_SODR = ul_pin_pos;
PIOC->PIO_OER = ul_pin_pos;
PIOC->PIO_PER = ul_pin_pos;
       
ul_pin_pos = PIO_PD0 | PIO_PD1 | PIO_PD2 | PIO_PD3 | PIO_PD6 | PIO_PD9 | PIO_PD10;
PIOD->PIO_IDR = ul_pin_pos;
PIOD->PIO_MDDR = ul_pin_pos;
PIOD->PIO_SODR = ul_pin_pos;
PIOD->PIO_OER = ul_pin_pos;
PIOD->PIO_PER = ul_pin_pos;
     
/* Making synchronous write working for connected pins */
PIOA->PIO_OWER = PIO_PA7 | PIO_PA14 | PIO_PA15;
PIOB->PIO_OWER = PIO_PB26;
PIOC->PIO_OWER = PIO_PC1 | PIO_PC2 | PIO_PC3 | PIO_PC4 | PIO_PC5;
PIOD->PIO_OWER = PIO_PD0 | PIO_PD1 | PIO_PD2 | PIO_PD3 | PIO_PD6 | PIO_PD9 | PIO_PD10;

引脚初始化与LED初始化相同,使用相同的寄存器,我想不需要解释。另一方面,底部有“使连接引脚同步写入工作”部分,需要一些解释。

通常在ARM中,如果你需要输出高电平,你写入1到“设置”寄存器。如果你需要输出低电平,你写入1到“清除”寄存器。所以你总是写入1,无论它是低电平还是高电平。这是一种非常巧妙的技术,因为你不需要获取以前的寄存器值来屏蔽掉你不想改变的位。例如,这就是你在AVR微控制器中所做的。设置寄存器和清除寄存器有相同的名称,只是字母S或C不同。但对于我们和TFT,我们不想先输出所有高电平,然后输出所有低电平,我们想一次性完成所有操作——这叫做“同步写入”。它允许在PIO_OWER寄存器中,之后我们使用PIO_ODSR寄存器而不是PIO_SODR(设置)和PIO_CODR(清除)寄存器。

保存system_sam3xa.c文件并返回SSD1289.h文件。

接下来我将定义16位像素组成的宏

#define RGB(red, green, blue)  ((uint16_t)(((red >> 3)<<11) | ((green >> 2)<<5) | (blue >> 3)))

要创建像素,需要指定三个颜色分量。请注意,它会丢弃颜色分量上的最低位,因为无法将 8+8+8 放入 16 位像素中:3 位红色和蓝色以及 2 位绿色被移走了。使用示例

RGB(100, 25, 0);

最后,我声明所有必要的函数

void LCD_WrCmd(uint16_t);
void LCD_WrDat(uint16_t);
void LCD_WaitMs(uint32_t ms);
void LCD_Init(void);
void LCD_SetCursor(uint16_t x, uint16_t y);
void LCD_SetArea(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);

现在我们准备移动到 SSD1289.c 文件并实现这些函数。LCD_WrCmd 用于向显示器发送命令,注意它如何使用 RS 位。

void LCD_WrCmd(uint16_t cmd)
{
    LCD_CLR_CS();                //Enable LCD communication
    LCD_CLR_RS();

    LCD_SET_DB(cmd);             //Set command on LCD pins
    
    LCD_CLR_WR();                //Transfer command in
    LCD_SET_WR();

    LCD_SET_CS();                //Disable LCD communication
    LCD_SET_RS();                //Back to data mode
}

LCD_WrDat用于向显示器发送数据。请注意,它与上一个的区别在于,此函数不使用RS位,因为数据模式是默认模式。

void LCD_WrDat(uint16_t val)
{
    LCD_CLR_CS();                //Enable LCD communication

    LCD_SET_DB(val);             //Set data on LCD pins
    
    LCD_CLR_WR();                //Transfer data in
    LCD_SET_WR();

    LCD_SET_CS();                //Disable LCD communication
}

LCD_WaitMs 仅被下一个 LCD_Init 函数需要。它以毫秒为单位进行延迟,以便为显示器提供执行给定命令所需的时间。实现中使用了 Arduino Due 以 84MHz 运行的事实。1 毫秒内有 84000 个时钟周期,NOP 操作和循环需要 5 个时钟周期,因此 84000/5 = 16800。这里的暂停是近似的,因为我不需要任何精度。

void LCD_WaitMs(uint32_t ms)
{
    uint32_t i;

    while (ms-- > 0)
    {
        for (i = 0; i < 16800; ++i)
            __asm volatile("NOP");
    }
}

LCD_Init 是一个初始化函数,它也会重置显示器。它发送所有必要的命令和数据以使显示器工作。我不会解释这些命令,因为我自己也不完全理解其中一些,而且这超出了本教程的范围。如果您想尝试,请参阅源代码,其中有一些注释。

LCD_SetCursor 是一个重要的函数。一旦我检测到视频帧变化,我将使用它将光标(一个像素)设置到起始位置。新帧将始终从起始位置开始。此函数仅发送适当的命令来执行光标位置更改操作。

void LCD_SetCursor(uint16_t x, uint16_t y)
{
    LCD_WrCmd(0x4E);    //Sets GDDRAM X address counter
    LCD_WrDat(x);
    
    LCD_WrCmd(0x4F);    //Sets GDDRAM Y address counter
    LCD_WrDat(y);
    
    LCD_WrCmd(0x22);    //Applies above set values
}

最后一个函数是LCD_SetArea。它用于指定视频帧输出的显示区域。一旦指定了区域,就可以逐个写入像素,显示器会自动递增到下一个位置。一旦达到最后一个位置,下一个像素将写入第一个位置,依此类推。

void LCD_SetArea(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
    LCD_WrCmd(0x44); LCD_WrDat((x2 << 8) | x1);        //Horizontal start and end positions
    LCD_WrCmd(0x45); LCD_WrDat(y1);                    //Vertical start position
    LCD_WrCmd(0x46); LCD_WrDat(y2);                    //Vertical end position
    
    LCD_SetCursor(x1, y1);
}

TFT显示器测试

为了测试上述函数,我将编写测试代码,定期用各种纯色填充整个显示器矩形。将有一个包含多个像素颜色信息的数组,测试代码将遍历它以选择某种颜色并用于填充。一旦数组结束,它将重新开始。颜色数组看起来像这样

#define WHITE        RGB(0xFF, 0xFF, 0xFF)
#define RED          RGB(0xFF, 0x00, 0x00)
#define GREEN        RGB(0x00, 0xFF, 0x00)
#define BLUE         RGB(0x00, 0x00, 0xFF)
#define BLACK        RGB(0x00, 0x00, 0x00)
uint16_t arColors[5] = {WHITE, RED, GREEN, BLUE, BLACK};

最终的main文件代码

#include "sam.h"
#include "UART.h"
#include "SSD1289.h"

#define WHITE        RGB(0xFF, 0xFF, 0xFF)
#define RED          RGB(0xFF, 0x00, 0x00)
#define GREEN        RGB(0x00, 0xFF, 0x00)
#define BLUE         RGB(0x00, 0x00, 0xFF)
#define BLACK        RGB(0x00, 0x00, 0x00)
uint16_t arColors[5] = {WHITE, RED, GREEN, BLUE, BLACK};

uint8_t ColorIndex;    
uint32_t SysTickCounter;

void ShowColor(void)
{
    for(uint32_t i=0; i<76800; i++)
    {
        LCD_SET_DB(arColors[ColorIndex]);
        LCD_CLR_WR();
        LCD_SET_WR();
    }
    
    ColorIndex ++;
    if(ColorIndex == 5)
        ColorIndex = 0;
}

void ToggleLEDs(void)
{
    uint32_t Pin_LEDRX = 1 << (PIO_PC30_IDX & 0x1Fu);
    uint32_t Pin_LEDTX = 1 << (PIO_PA21_IDX & 0x1Fu);
        
    if(PIOC->PIO_PDSR & Pin_LEDRX)
    {
        PIOC->PIO_CODR = Pin_LEDRX;    //Lights on
        PIOA->PIO_CODR = Pin_LEDTX;
    }
    else
    {
        PIOC->PIO_SODR = Pin_LEDRX;    //Lights off
        PIOA->PIO_SODR = Pin_LEDTX;
    }
}

void SysTick_Handler(void)
{
    SysTickCounter ++;
    if(1000 == SysTickCounter)
    {
        WDT->WDT_CR = WDT->WDT_CR | WDT_CR_KEY_PASSWD | WDT_CR_WDRSTT;    //Re-load watchdog
        
        SysTickCounter = 0;
        ToggleLEDs();
        ShowColor();
    }
}

int main(void)
{
    SystemInit();
    UART_Init();    
    LCD_Init();
    
    ColorIndex = 0;                   //Current index in arColor array
    LCD_SetArea(0, 0, 239, 319);      //Setting working area
    LCD_CLR_CS();                     //Enable LCD
    LCD_SET_RS();                     //Data write mode
    
    while(1);
}

请注意,我移除了第2部分中的测试代码(打印函数),并提取了ToggleLEDs函数。构建、上传并观察。它应该看起来像此视频中我的效果。

 结论

这是最后一篇准备文章。所有必要的工具都已编写,我们已准备好处理 USB。您可能需要阅读一些 USB 2.0 规范,因为我无法完整解释 USB,尽管我会尝试给出我的简短版本。

源代码在此。

第4部分在此

更新 2015-07-10:重新上传了不包含Debug文件夹的源代码。

在Arduino Due上获取USB网络摄像头的视频流 - 第3部分:看门狗定时器和TFT显示器 - CodeProject - 代码之家
© . All rights reserved.