【DigiKey创意大赛】便携生命探测仪03+ILI9488驱动DMA实现和简单界面
[复制链接]
【DigiKey创意大赛】便携生命探测仪03+ILI9488驱动DMA实现和简单界面
上一帖介绍了热成像相机软硬件开发环境搭建,并根据查到的一些资料初步实现了热成像基本功能。但是由于我使用的是以前别的测评留下的屏幕,在刷屏时不能使用DMA导致显示帧率很慢,留下一个问题待解决。参见本帖最后。
本来想着再买个支持SPI 16bit DMA的屏,或者换其他带屏幕的平台开发板。但是作为DIY老鸟,本着电子垃圾能用就绝不浪费的原则,我对开源显示驱动库TFT_eSPI进行详细研究。虽然该库作者Bodmer在GitHub上回复说,即使勉强使用DMA,但是16bit转18bit的CPU开销会抵消掉DMA带来的优势,但是我这个热成像相机的应用和普通的显示不一样,这个需要用到伪彩色,也就是从调色板选部分颜色就行,不用显示全部16bit色彩范围,这就给我留出来操作空间了。
这里说的伪彩色就是选一组渐变色来表示被测物不同的温度,常见的渐变色如下图。
我这里参考Adafruit_MLX90640的库,设置了256色渐变色作为温度标识,效果接近上图第二个。Adafruit_MLX90640的库提供的是16bit调色板,我需要先转换成18bit调色板,考虑ILI9488在DMA传输时是按照字节送数据,我把每个颜色存成3个字节,RGB顺序按照ILI9488的时序来,就能在DMA自动执行数据传送时不用倒腾字节顺序。时序图如下。
然后就是参照TFT_eSPI库重新写一个8bit的DMA送数据函数,如下图。
最后就是显示的时候,先设置显示窗口,再调用这个函数。其中要注意的是,DMA每次传输不能超过64KB字节,我这里为了省内存,把一帧数据分成了16份,远远小于64KB,代码如下图。
经过这样更改后,显示文字和图片还是采用TFT_eSPI库的接口函数,显示热成像图片使用新做的这个函数,既保证了兼容,还提高了效率。实测效果还不错,CPU纯跑刷屏任务可以到每秒十多帧,完全满足热成像的速度了。实际演示效果见开头视频。
显示搞定后,我做了一个简单的界面,如下图。
热成像占据左边大部分屏幕。上面是320*240点阵的热成像动态图,中间是调色板温度范围指示条。最下面显示最高温度。右半边目前留白,后面计划添加另外一个传感器数据。
本次设计采用的是Adafruit_MLX90640的库,具体代码如下。
#ifndef MXL_H
#define MXL_H
#include <Arduino.h>
#include <lcd.h>
#define MINTEMP 26
#define MAXTEMP 35
#define MLX_MIRROR 0 // Set 1 when the camera is facing the screen
#define FILTER_ENABLE 1 // 滤波器使能
#define INTERPOLATION_ENABLE 1 // 差值使能
extern uint8_t CamBuffer[CAM_W * CAM_H]; // 存储量化后的8位像素温度数据
extern bool lock; // 简单的锁,防止拷贝温度数据的时候对内存的访问冲突
void task_mlx(void *ptr);
#endif // MXL_H
#include <Arduino.h>
#include <mxl.h>
#include <Wire.h>
#include <Adafruit_MLX90640.h>
#include <lcd.h>
#define I2C_SCL SCL
#define I2C_SDA SDA
#define MLX_I2C_ADDR 0x33
Adafruit_MLX90640 mlx;
float frame[32 * 24]; // buffer for full frame of temperatures
float temp_frame[32 * 24];
uint16_t inter_p[320 * 240];
void MxlInit(void)
{
// I2C init
Wire.begin(I2C_SDA, I2C_SCL);
byte error, address;
Wire.beginTransmission(MLX_I2C_ADDR);
error = Wire.endTransmission();
if (error == 0)
{
Serial.print(F("I2C device found at address 0x"));
Serial.print(MLX_I2C_ADDR, HEX);
Serial.println(F(" !"));
}
else if (error == 4)
{
Serial.print(F("Unknown error at address 0x"));
Serial.println(MLX_I2C_ADDR, HEX);
}
Serial.println(F("Adafruit MLX90640 Simple Test"));
if (!mlx.begin(MLX90640_I2CADDR_DEFAULT, &Wire))
{
Serial.println(F("MLX90640 not found!"));
while (1)
delay(10);
}
// mlx.setMode(MLX90640_INTERLEAVED);
mlx.setMode(MLX90640_CHESS);
mlx.setResolution(MLX90640_ADC_18BIT);
mlx90640_resolution_t res = mlx.getResolution();
mlx.setRefreshRate(MLX90640_16_HZ);
mlx90640_refreshrate_t rate = mlx.getRefreshRate();
Wire.setClock(800000); // max 1 MHz
// image init
for (int i = 0; i < 320 * 240; i++)
{
inter_p[i] = 255;
}
Serial.println("image init ok");
// frame init
for (int i = 0; i < 32 * 24; i++)
{
temp_frame[i] = MINTEMP;
}
Serial.println("frame init ok");
Serial.println(F("All init over."));
}
// 消抖并翻转
// Filter temperature data and change camera direction
void filter_frame(float *in, float *out)
{
if (MLX_MIRROR == 1)
{
for (int i = 0; i < 32 * 24; i++)
{
if (FILTER_ENABLE == 1)
out[i] = (out[i] + in[i]) / 2;
else
out[i] = in[i];
}
}
else
{
for (int i = 0; i < 24; i++)
for (int j = 0; j < 32; j++)
{
if (FILTER_ENABLE == 1)
out[32 * i + 31 - j] = (out[32 * i + 31 - j] + in[32 * i + j]) / 2;
else
out[32 * i + 31 - j] = in[32 * i + j];
}
}
}
// float to 0,255
int map_f(float in, float a, float b)
{
if (in < a)
return 0;
if (in > b)
return 255;
return (int)((in - a) * 255 / (b - a));
}
// 32*24插值10倍到320*240
// Transform 32*24 to 320 * 240 pixel
void interpolation(float *data, uint16_t *out)
{
for (uint8_t h = 0; h < 24; h++)
{
for (uint8_t w = 0; w < 32; w++)
{
out[h * 10 * 320 + w * 10] = map_f(data[h * 32 + w], MINTEMP, MAXTEMP);
}
}
for (int h = 0; h < 240; h += 10)
{
for (int w = 1; w < 310; w += 10)
{
for (int i = 0; i < 9; i++)
{
out[h * 320 + w + i] = (out[h * 320 + w - 1] * (9 - i) + out[h * 320 + w + 9] * (i + 1)) / 10;
}
}
for (int i = 0; i < 9; i++)
{
out[h * 320 + 311 + i] = out[h * 320 + 310];
}
}
for (int w = 0; w < 320; w++)
{
for (int h = 1; h < 230; h += 10)
{
for (int i = 0; i < 9; i++)
{
out[(h + i) * 320 + w] = (out[(h - 1) * 320 + w] * (9 - i) + out[(h + 9) * 320 + w] * (i + 1)) / 10;
}
}
for (int i = 0; i < 9; i++)
{
out[(231 + i) * 320 + w] = out[230 * 320 + w];
}
}
}
// Quick sort
void qusort(float s[], int start, int end) // 自定义函数 qusort()
{
int i, j; // 定义变量为基本整型
i = start; // 将每组首个元素赋给i
j = end; // 将每组末尾元素赋给j
s[0] = s[start]; // 设置基准值
while (i < j)
{
while (i < j && s[0] < s[j])
j--; // 位置左移
if (i < j)
{
s[i] = s[j]; // 将s[j]放到s[i]的位置上
i++; // 位置右移
}
while (i < j && s[i] <= s[0])
i++; // 位置左移
if (i < j)
{
s[j] = s[i]; // 将大于基准值的s[j]放到s[i]位置
j--; // 位置左移
}
}
s[i] = s[0]; // 将基准值放入指定位置
if (start < i)
qusort(s, start, j - 1); // 对分割出的部分递归调用qusort()函数
if (i < end)
qusort(s, j + 1, end);
}
// The camera allows up to four non-consecutive bad points with a value of nan.
void bug_fix(float *frame)
{
for (int i = 0; i < 768 - 1; i++)
{
if (isnan(frame[i]))
frame[i] = frame[i + 1];
}
}
uint32_t runtime = 0;
int fps = 0;
float max_temp = 0.0;
int record_index = 0;
// 热成像读取多任务
void task_mlx(void *ptr)
{
int i = 0;
MxlInit();
while (true)
{
// 获取一帧
// Get a frame
if (mlx.getFrame(frame) != 0)
{
Serial.println(F("Get frame failed"));
return;
}
bug_fix(frame);
// 和上次结果平均,滤波
// Filter temperature data
filter_frame(frame, temp_frame);
// 快排
qusort(frame, 0, 32 * 24 - 1);
max_temp = frame[767];
MaxTemp = max_temp;
// Serial.println(max_temp);
if (INTERPOLATION_ENABLE == 1)
{
// 温度矩阵转换图像矩阵,将32*24插值到320*240
// Display with 320*240 pixel
interpolation(temp_frame, inter_p);
lock = true;
i = 0;
while (i < 76800)
{
CamBuffer[i] = inter_p[i];
i++;
}
lock = false;
}
else
{
// Display with 32*24 pixel
for (uint8_t h = 0; h < 24; h++)
{
for (uint8_t w = 0; w < 32; w++)
{
uint8_t colorIndex = map_f(temp_frame[h * 32 + w], MINTEMP, MAXTEMP);
for (uint8_t y = 0; y < 10; y++)
{
for (uint8_t x = 0; x < 10; x++)
{
CamBuffer[(h * 10 + y) * 320 + w * 10 + x] = colorIndex;
}
}
}
}
}
}
vTaskDelete(NULL);
}
目前还有一个问题,就是屏幕显示的温度图像有纵向干扰条纹,我用了不同库读取数据都存在,初步怀疑是传感器的问题,但是手头只有这一个宝贝,还没办法验证,等我再查查资料,看有没有解决办法。
下一步添加另外一个传感器Adafruit BME680的读取和显示。传感器如下图。
|