项目背景
首先由衷的祝愿电子森林发展的越来越好,自从参与Funpack项目以来,接触了越来越多的开发板,也拓宽了很多知识面,所以无需犹豫,果断参与第四期。
第四期的主题是点灯,但考虑到I.MXRT1062这么丰富的外设,只点个灯确实有点浪费,于是尝试用Teensy驱动屏幕,制作一个模拟赛车的仪表,显示赛车的实时速度信息。
项目详情
Teensy4.1(以下简称Teensy)使用I.MXRT1062作为主控,主控USB上位机进行通讯,上位机通过UDP协议获取赛车游戏中车辆的遥测数据,并通过USB HID发给Teensy,Teensy上的I.MXRT1062通过SPI驱动LCD,并根据遥测数据中的车辆速度更新屏幕上的内容,项目框图如下:
设计思路
- 上位机通过UDP与游戏建立通讯,获取到游戏中赛车的实时遥测数据
- Teensy有USB接口,可以连接到上位机
- 上位机和Teensy之间通过USB HID通讯,并传递遥测数据
- Teensy的SPI接口可以驱动GC9A01A的圆形屏幕,并根据遥测数据更新屏幕上显示的内容
硬件介绍
芯片介绍
- ARM Cortex-M7 at 600 MHz
- Float point math unit, 64 & 32 bits
- 7936K Flash, 1024K RAM (512K tightly coupled), 4K EEPROM (emulated)
- QSPI memory expansion, locations for 2 extra RAM or Flash chips
- USB device 480 Mbit/sec & USB host 480 Mbit/sec
- 55 digital input/output pins, 35 PWM output pins
- 18 analog input pins
- 8 serial, 3 SPI, 3 I2C ports
- 2 I2S/TDM and 1 S/PDIF digital audio port
- 3 CAN Bus (1 with CAN FD)
- 1 SDIO (4 bit) native SD Card port
- Ethernet 10/100 Mbit with DP83825 PHY
- 32 general purpose DMA channels
- Cryptographic Acceleration & Random Number Generator
- RTC for date/time
- Programmable FlexIO
- Pixel Processing Pipeline
- Peripheral cross triggering
- Power On/Off management
开发板介绍
没有获取到PCB图纸,不过肉眼看起来布局规整,整体小巧
软件流程
如上图所示,Teensy上电后首先启动USB,通过代码里配置的端点与上位机建立通讯,然后通过SPI驱动LCD显示,在while循环里等待上位机发送遥测数据,收到遥测数据后解析提取需要的速度信息,根据速度信息更新LCD的显示内容,从而完成模拟赛车仪表的开发。
开发流程
开发流程主要包括USB HID通讯建立、屏幕驱动显示、延迟优化三个部分,具体内容如下:
USB HID通讯建立
HID通讯的代码可以参考官方示例,示例配置足够本项目使用,因此不做调整
Teensy通过USB连接到电脑后,可以使用USBLyazer查找到具体的配置信息。
使用BUSHound可以确定具体用来通讯的IN OUT端点
官方示例中的USB是复合了RawHID和UART两个设备,其中UART用于终端信息显示
使用BUS Hound对OUT端点发送数据,可以Capture到 并且Teensy终端可以正常输出收到的信息,上位机就通过此端点向Teensy传输遥测数据
HID部分的代码官方已经帮我们配置好了 只需要在Arduino的IDE里面选择对应的配置即可
HID消息接收部分的代码如下:
void loop() {
int n;
uint16_t speed;
n = RawHID.recv(buffer, 0); // 0 timeout = do not wait
if (n > 0) {
// the computer sent a message. Display the bits
// of the first byte on pin 0 to 7. Ignore the
// other 63 bytes!
// Serial.print(F("Received packet, first byte: "));
// Serial.println(buffer[0],HEX);
// Serial.print(F("Received packet, Second byte: "));
// Serial.println(buffer[1],HEX);
speed = (buffer[0]<<8)+(buffer[1]);
if(last_speed!=speed)
{
updateSpeed(speed);
last_speed = speed;
}
// Serial.print(F("Speed:"));
// Serial.println(speed,DEC);
}
}
屏幕显示
本项目使用的屏幕是GC9A01A,需要用SPI进行通讯,Arduino有丰富的插件库,经过测试筛选,发现Adafruit的GFX和GC9A01A库可以驱动该屏幕,Teensy和屏幕的连接方式如下:
/* Hardware Connect2
Teensy4.1 TFT-LCD3
7 ---- BLK4
NC ---- RES5
9 ---- DC6
10 ---- CS7
13 ---- SCL8
11 ---- SDA9
GND ---- GND10
3V ---- VCC
*/
LCD驱动代码如下:
#define TFT_CS 10 // Chip select
#define TFT_DC 9 // Data/command
#define TFT_BL 7 // Backlight control
Adafruit_GC9A01A tft(TFT_CS, TFT_DC);
void setup() {
Serial.begin(9600);
Serial.println(F("RawHID Example"));
tft.begin();
#if defined(TFT_BL)
pinMode(TFT_BL, OUTPUT);
digitalWrite(TFT_BL, HIGH); // Backlight on
#endif // end TFT_BL
Serial.println(F("Benchmark Time (microseconds)"));
delay(10);
Serial.print(F("Screen fill "));
Serial.println(testFillScreen());
delay(500);
tft.setRotation(-90);
Serial.print(F("Text "));
racingSpeedInit();
delay(3000);
}
延迟优化
当要显示的内容有变化时,因为没有移植类似LVGL的显示框架来实现只针对变化的区域更新,如果采用默认的方式进行整屏擦除再写入,整个显示内容的更新会非常缓慢,整体的刷新率很低,给用户的直观感觉就是卡顿和延迟。
// //V1版本 延迟很高
// unsigned long updateSpeed(unsigned int speed) {
// //tft.fillScreen(GC9A01A_BLACK);
// unsigned long start = micros();
// tft.setCursor(80, 50);
// tft.setTextColor(GC9A01A_YELLOW); tft.setTextSize(3);
// tft.println("SPEED:");
// tft.setCursor(30, 90); //Clear Last Speed
// tft.setTextColor(GC9A01A_BLACK); tft.setTextSize(10);
// tft.println(" ");
// if(speed<10)
// {
// tft.setCursor(100, 90); //个位数时速x:30 两位数时速x:100
// }
// else if((speed>=10)&&(speed<100))
// {
// tft.setCursor(70, 90); //十位数时速x:30 两位数时速x:70
// }
// else
// {
// tft.setCursor(30, 90); //百位数时速x:30 两位数时速x:30
// }
// tft.setTextColor(GC9A01A_YELLOW); tft.setTextSize(10);
// tft.println(speed);
// return micros() - start;
// }
再次基础上进行优化,取消整屏擦除,只是对上一次更新的区域进行擦除,然后再进行新一轮内容的更新,例如上一次的速度是100Km/h,本次更新的时候先用黑色把上次的100覆盖掉,再写入本次速度,这样就能显著提升内容更新效率,显著降低延迟。
//V2版本 显著降低延迟
unsigned long updateSpeed(unsigned int speed) {
unsigned long start = micros();
if(disp_update_cnt>1)
{
if(last_disp_speed<10)
{
tft.setCursor(100, 90); //个位数时速x:30 两位数时速x:100
}
else if((last_disp_speed>=10)&&(last_disp_speed<100))
{
tft.setCursor(70, 90); //十位数时速x:30 两位数时速x:70
}
else
{
tft.setCursor(30, 90); //百位数时速x:30 两位数时速x:30
}
tft.setTextColor(GC9A01A_BLACK); tft.setTextSize(10);
tft.println(last_disp_speed);
}
if(speed<10)
{
tft.setCursor(100, 90); //个位数时速x:30 两位数时速x:100
}
else if((speed>=10)&&(speed<100))
{
tft.setCursor(70, 90); //十位数时速x:30 两位数时速x:70
}
else
{
tft.setCursor(30, 90); //百位数时速x:30 两位数时速x:30
}
tft.setTextColor(GC9A01A_YELLOW); tft.setTextSize(10);
tft.println(speed);
last_disp_speed = speed;
disp_update_cnt++;
return micros() - start;
}
当然了,上面的代码可以进一步优化,比如上次速度是108本次速度是102,那只需对个位进行擦除重新写入即可,在这里就只提供思路了。
项目展示
项目演示视频已上传到B站,链接如下:
项目演示