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






4.71/5 (6投票s)
看门狗定时器和 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文件夹的源代码。