前阵子公司有一个基于毒品检测的项目需要做一个曲线显示的功能,由于这块是我的技能短板,因为我之前搞软件的应用,逻辑,框架,架构设计这块比较多,而我师弟在底层方面非常精通,所以把这一块核心的功能交给了我师弟,让他帮忙来实现基本的库,然后我基于他的库完成产品所需要的功能。
又恰好在项目之前,RT-Thread发起了一个基于RT-Thread Nano的Mini示波器
DIY的活动,作为RT-Thread社区工作小组一员的我,有幸能看到这个项目从头到尾的制作过程,也从中学习了LCD曲线数据处理和显示的一些思想。
活动链接如下:
【DIY活动】一起来做一个基于RT-Thread Nano的Mini示波器吧!
完成曲线显示大致需要以下三个步骤:
- 1、数据采集
- 2、数据处理
- 3、数据显示
废话不多说,咱们先看下显示效果:
严格意义上来说,小熊派这种SPI屏其实不太适合用来刷曲线,首先分辨率太低了,还有就是SPI的速率也不高,如果曲线显示条件苛刻一点,很容易导致LCD显示闪屏现象,体验感非常不好,但是针对传感器数据显示我们还是有能力实现的。
于是,我们需要对屏驱动做一些最基本的优化:
1、优化LCD驱动
1、提升刷屏速度
由于要刷曲线,所以只能想办法尽量提升屏的刷新速度,于是在LCD手册里有这么一个寄存器,可以提升屏的刷新速度:
在LCD驱动初始化代码里,这个寄存器默认配置的是60Hz,也就是0x0F这个值
/* Frame Rate Control in Normal Mode */ LCD_Write_Cmd(0xC6); // LCD_Write_Data(0x0F); //60HZ LCD_Write_Data(0x01); //111Hz 提升屏的刷新速度
本来设置为0x00为119Hz,但是设置完LCD就黑屏了,改为0x01就不会,目前没找到具体原因,可能这是屏固件的BUG,暂时将就着用吧;或者有朋友知道的,感谢在留言区分享给我。
2、改用寄存器发送
/** * @brief LCD底层SPI发送数据函数 * * @param data 数据的起始地址 * @param size 发送数据大小 * * @return void */ static void LCD_SPI_Send(uint8_t *data, uint16_t size) { for(int i = 0 ; i < size ; i++) { *((uint8_t*)&hspi2.Instance->DR) = data[i]; while(__HAL_SPI_GET_FLAG(&hspi2, SPI_FLAG_TXE) != 1) {} } }
HAL库的HAL_SPI_Transmit
函数发送会慢一些,改用寄存器发送会更快。
2、曲线显示逻辑
要在LCD上显示曲线,大家可能就会有这样的疑问:
我的数据可能上千,几万这样,如何转换成对应屏分辨率的显示?到底从哪里开始显示?怎么显示?
有一种比较好的思路,就是定义一个固定长度的数组,每次往数组尾部不断的更新数据,然后该数据会不断的往前推,这其实就是我们说的fifo(环形缓冲)队列,然后定义一个新的备份缓冲区,在这个备份缓冲区里找到数据的最大值以及最小值,求出针对LCD分辨率的缩放系数,根据计算结果将备份缓冲区用于LCD显示,这就是根据实际情况进行的缩放,也叫做局部缩放。以下是这个例程的曲线数据结构:
#define DATA_SIZE 240 /*曲线显示区域,即相对于LCD的宽度,X轴*/ #define PLOT_DISPLAY_AREA_X 51 /*曲线显示区域,即相对于LCD的高度,Y轴*/ #define PLOT_DISPLAY_AREA_Y 210 #define LCD_X 240 #define LCD_Y 240 /*曲线处理*/ typedef struct { /*实时曲线数据*/ uint16_t rel_data_data[DATA_SIZE]; /*旧的曲线数据*/ uint16_t old_plot_data[DATA_SIZE]; /*新的曲线数据*/ uint16_t new_plot_data[DATA_SIZE]; } plot_data_handler ; extern plot_data_handler plot_handler ;
由于要做到一次性更新避免闪屏,这里定义了三个缓冲区,rel_data_data
用于更新实时数据,old_plot_data
为旧的已经处理的显示数据,new_plot_data
为刚刚处理的显示数据,相当于双缓冲效果。
3、曲线显示实现
3.1 数据采样部分
由于刚开始显示,曲线的数据缓存是空的,所以我们要做一下初始化,保证曲线能够直接显示出来:
smoke_value = mq2_sensor_interface.get_smoke_value(&mq2_sensor_interface); for(int i = 0 ; i < DATA_SIZE ; i++) plot_handler.rel_data_data[i] = smoke_value ; memcpy(plot_handler.new_plot_data, plot_handler.rel_data_data, sizeof(plot_handler.new_plot_data)); memcpy(plot_handler.old_plot_data, plot_handler.new_plot_data, sizeof(plot_handler.new_plot_data));
接下来就是显示逻辑上提到的,我们需要有一个环形缓冲,不断的追加数据:
smoke_value = mq2_sensor_interface.get_smoke_value(&mq2_sensor_interface) ; /*更新数据到队列*/ for(i = 0 ; i <= DATA_SIZE - 2 ; i++) plot_handler.rel_data_data[i] = plot_handler.rel_data_data[i + 1]; plot_handler.rel_data_data[DATA_SIZE - 1] = smoke_value ;
这样我们就完成了最基本的数据采样部分。
3.2 数据处理部分
数据处理我定义了以下函数来实现:
void LCD_Plot_Remap(uint16_t *cur_data, uint16_t *backup_data, uint16_t cur_data_size)
cur_data表示当前的实时数据包
backup_data表示备份数据包
cur_data_size表示数据包的长度
实时数据包就是没有经过处理的数据包,备份数据包就是经过处理的数据包。
在这个函数中主要完成了找实时数据包的最大、最小值、计算缩放系数:
最大值查找:
value = 0 ; for(i = 0; i < cur_data_size; i++) if(cur_data[i] > value) value = cur_data[i]; max = value ;
最小值查找:
value = cur_data[0]; for(i = 0; i < cur_data_size; i++) if(cur_data[i] < value) value = cur_data[i]; min = value ;
缩放系数的计算:
max_min_diff = (float)(max - min); scale = (float)(max_min_diff / height);
将处理的结果拷贝到备份数据包中。
完整实现如下:
/* cur_data:当前要显示的曲线数据包 cur_data_size:当前要显示的曲线数据包的大小 */ void LCD_Plot_Remap(uint16_t *cur_data, uint16_t *backup_data, uint16_t cur_data_size) { uint32_t i = 0 ; float temp = 0; /*数据包最大值*/ uint16_t max = 0; /*数据包最小值*/ uint16_t min = 0; float scale = 0.0; uint16_t value = 0; float max_min_diff = 0.0; /*曲线显示的高度*/ float height = PLOT_DISPLAY_AREA_Y; char display_rel_buf[20] = {0}; char display_max_buf[20] = {0}; char display_min_buf[20] = {0}; char display_sub_buf[20] = {0}; /*显示X坐标轴*/ for(uint8_t i = PLOT_DISPLAY_AREA_X-1 ; i < 240 ; i++) LCD_Draw_ColorPoint(i, 239, RED); /*显示Y坐标轴*/ for(uint8_t i = LCD_Y-PLOT_DISPLAY_AREA_Y ; i < 240 ; i++) LCD_Draw_ColorPoint(PLOT_DISPLAY_AREA_X-1, i, RED); value = 0 ; for(i = 0; i < cur_data_size; i++) if(cur_data[i] > value) value = cur_data[i]; max = value ; value = cur_data[0]; for(i = 0; i < cur_data_size; i++) if(cur_data[i] < value) value = cur_data[i]; min = value ; sprintf(display_rel_buf,"%04d",cur_data[DATA_SIZE-1]); sprintf(display_max_buf,"%04d",max); sprintf(display_min_buf,"%04d",min); sprintf(display_sub_buf,"%04d",max-min); LCD_ShowString(5,LCD_Y-PLOT_DISPLAY_AREA_Y+10,LCD_X,16,16,"rel:"); LCD_ShowString(5,LCD_Y-PLOT_DISPLAY_AREA_Y+20+10,LCD_X, 16, 16, display_rel_buf); LCD_ShowString(5,LCD_Y-PLOT_DISPLAY_AREA_Y+50+10,LCD_X,16,16,"max:"); LCD_ShowString(5,LCD_Y-PLOT_DISPLAY_AREA_Y+70+10,LCD_X, 16, 16, display_max_buf); LCD_ShowString(5,LCD_Y-PLOT_DISPLAY_AREA_Y+100+10,LCD_X,16,16,"min:"); LCD_ShowString(5,LCD_Y-PLOT_DISPLAY_AREA_Y+120+10,LCD_X, 16, 16, display_min_buf); LCD_ShowString(5,LCD_Y-PLOT_DISPLAY_AREA_Y+150+10,LCD_X,16,16,"sub:"); LCD_ShowString(5,LCD_Y-PLOT_DISPLAY_AREA_Y+170+10,LCD_X, 16, 16, display_sub_buf); if(min > max) return ; max_min_diff = (float)(max - min); scale = (float)(max_min_diff / height); if(cur_data_size < DATA_SIZE) return; for(i = 0; i < DATA_SIZE; i ++) { temp = cur_data[i] - min; backup_data[i] = DATA_SIZE - (uint16_t)(temp / scale) - 1; } }
3.3 数据显示部分
这部分应该是最激动人心的,但是它的实现却是最简单的,就是将数据处理部分得到的备份数据包里的每一个数据依次用线段连接起来即可,为了让驱动更快一些,以下的处理采用寄存器发送:
/*显示曲线*/ void LCD_Plot_Display(uint16_t *pData, uint32_t size, uint16_t color) { uint32_t i, j; uint8_t color_L = (uint8_t) color; uint8_t color_H = (uint8_t) (color >> 8); if(size < DATA_SIZE) return ; for (i = PLOT_DISPLAY_AREA_X; i < DATA_SIZE - 1; i++) { if (pData[i + 1] >= pData[i]) { LCD_Address_Set(i, pData[i], i, pData[i + 1]); LCD_DC(1); for (j = pData[i]; j <= pData[i + 1]; j++) { *((uint8_t*) &hspi2.Instance->DR) = color_H; while (__HAL_SPI_GET_FLAG(&hspi2, SPI_FLAG_TXE) != 1); *((uint8_t*) &hspi2.Instance->DR) = color_L; while (__HAL_SPI_GET_FLAG(&hspi2, SPI_FLAG_TXE) != 1); } } else { LCD_Address_Set(i, pData[i + 1], i, pData[i]); LCD_DC(1); for (j = pData[i + 1]; j <= pData[i]; j++) { *((uint8_t*) &hspi2.Instance->DR) = color_H; while (__HAL_SPI_GET_FLAG(&hspi2, SPI_FLAG_TXE) != 1); *((uint8_t*) &hspi2.Instance->DR) = color_L; while (__HAL_SPI_GET_FLAG(&hspi2, SPI_FLAG_TXE) != 1); } } } }
4、传感器数据实时曲线显示
实现逻辑如下:
while (1) { smoke_value = mq2_sensor_interface.get_smoke_value(&mq2_sensor_interface) ; /*更新数据到队列*/ for(i = 0 ; i <= DATA_SIZE - 2 ; i++) plot_handler.rel_data_data[i] = plot_handler.rel_data_data[i + 1]; plot_handler.rel_data_data[DATA_SIZE - 1] = smoke_value ; /*先将背景刷黑*/ LCD_Plot_Display(plot_handler.old_plot_data, DATA_SIZE, BLACK); /*传感器数据处理*/ LCD_Plot_Remap(plot_handler.rel_data_data,plot_handler.new_plot_data, DATA_SIZE); /*传感器数据曲线显示*/ LCD_Plot_Display(plot_handler.new_plot_data, DATA_SIZE, GREEN); /*将处理完成的备份数据区的数据拷贝到旧的备份数据区*/ memcpy(plot_handler.old_plot_data, plot_handler.new_plot_data, sizeof(plot_handler.new_plot_data)); HAL_Delay(10); }
本节代码已同步到码云的代码仓库中,获取方法如下:
1、新建一个文件夹
2、使用git clone远程获取该项目
项目开源仓库:
https://gitee.com/morixinguan/bear-pi
我还将之前做的一些项目以及练习例程在近期内全部上传完毕,与大家一起分享交流: