Skip to content

Latest commit

 

History

History
560 lines (424 loc) · 37.3 KB

File metadata and controls

560 lines (424 loc) · 37.3 KB

\mainpage LTDC版本的室内控制面板程序

LTDC版本的室内控制面板程序

开发日志 2019/09/26

遇到问题:图片出现雪花点错位

找资料时发现有人用LTDC和DMA2D显示图片时出现了问题 博客地址
主要是显示的图片前面有雪花点,并且显示的图片有错位。
图片显示错位

雪花点的出现

  • 原因分析
    LTDC的显存中被写入了“其他数据”,一些不是目标图片的数据。这些数据是其他语句在执行过程中所产生的(如变量、堆栈等等),它们并不是按照特定格式排列的(像素数据需要按RGB888、RGB565等格式排列)。
    当这些数据被写到显存中的时候,LTDC将它看成颜色数据并显示出来,所以显示出来的是杂色雪花点(有颜色是因为该内存中的数据恰好三个字节连在一起组成了RGB颜色值,所以在显示屏中有显示颜色)

图片的错位显示

仔细观察图片,会发现错位的图片中右边“C”上面的短黑线是比左边的长黑线要高一个像素的,这两条线不是在同一行。说明图片并不是单纯的将左半边的部分平移到右边来了而已(乍一看的话很容易产生这种错觉)。
而出现这种情况的原因,跟DMA2D的工作方式有关。

DMA2D

DMA2D中关于图片的大小主要有三个关键的参数:

DMA2D_InitStruct.DMA2D_PixelPerLine // 指定一行的像素数,即图片的宽度
DMA2D_InitStruct.DMA2D_NumberOfLine // 指定行的数量,即图片的高度
DMA2D_InitStruct.DMA2D_OutputOffset // 一行的末尾到下一行起始的偏移量,值为LCD的宽度减去图片的宽度

其中,DMA2D_OutputOffset是比较难理解的。为什么需要这么一个参数?这跟像素数据的存储有关。像素数据是按照特定排列存储在显存里的,而内存都是按照顺序存储的,先是按序存储一行的数据,然后紧接着存储下一行的数据。
这就导致了与我们的固有观念产生了极大的冲突:在显示屏上相邻的像素点,在存储位置上并不一定相邻。

例如:对于像素点p(x,y),与他相邻的像素点p1(x+1,y)和p2(x-1,y)在存储位置上是相邻的(因为在同一行);但是相邻像素点p3(x,y+1)跟p不在同一行,它们在存储位置上相差了(LCD_PIXEL_WIDTH - 1)个距离。类比一下二维数组,更好理解。

所以,画一条竖线,在内存中并不是像画横线一样操作了一片连续的的存储空间,而是操作了一个像素点便跳转(LCD_PIXEL_WIDTH - 1)个像素点去操作下一个像素点,各个“离散的”像素点在LCD屏幕有序地显示出来,最终组成了一条看起来“连续的”的竖线。

导致错位的原因

图片数据前面被“其他数据”占用了,导致图片数据的起始点不是原点!第一个像素点在某一行末尾的时候才被载入。由于DMA2D配置的是读取到一行图片的数据就换行(DMA2D_InitStruct.DMA2D_PixelPerLine),但是前面被“其他数据”占用了“一行图片”的一部分。虽然我们肉眼看起来图片没有读够一行,但是DMA2D认为它已经读够一行了,所以换行继续读取下一行的图片数据。

而第一行图片的数据已经被读取了一部分,这就导致DMA2D在读取第二行数据的时候,虽然我们肉眼看起来已经读完一行图片了,但是DMA2D没有读够“一行图片”长度的数据,于是他便继续读显存下一部分的数据,即第二行图片的前一部分(更好地说明了像素数据在显存中是一行一行连续存储的)。

如此重复多次,就出现了我们看到的“图片错位”的情况。

归因:冗杂数据从何而来

试错:不断地踩坑

一开始以为是LTDC、LTDC_Layer或DMA2D的存储空间没有配置好,导致“写入数据”的位置与“显示数据”的位置产生了偏差。以为是LTDC_Layer的显示区域往前挪,显示了“图片所在显存位置”之外的某些东西。但是查了很多配置,都没发现有什么问题。也改过内存位置,想将显示区域向后挪动,不显示前面的雪花点,结果都无功而返。
在写着这段文字的时候,我也才反应过来:根本不可能是显示区域错位的问题。我之所以会以为是显示错位的问题是因为我以为整个显示屏就那一小块显示图片的区域(图片在显示屏中间,其他地方都是黑的)。但实际上整个屏幕都在显示显存中的东西(其他区域也有数据,只不过是黑的而已,看起来就好像没数据一样!),所以不可能是显示区域错位的问题,要是错位的画其他黑色的地方也会错位,而不只是图片的那一小块区域。

图片地址指向错误的可能

既然不可能是图片显示错位,那么只可能是搬运了错误的数据到显存。
我一开始以为那一片雪花点是BMP图片的信息头,所以想着删掉图片数组的前一部分就能消掉雪花点了。但是我将图片数组的前一半都删掉了,前面雪花点区域的大小一点没变,而雪花点后面的图片倒是的确减少了一半,这让我百思不得其解 —— 这一部分雪花点究竟来源何处?

官方例程的对照

最后,我在搜资料的时候发现原来ST官方的标准库例程中有关于利用DMA2D显示图片的(我当初咋不知道!!在网上找了好久才发现有一开头的那个博客!!)。
于是我将博客上的例程与官方的例程逐行进行对照,乍一看都没啥问题,差点崩溃。不过在第N次检查时发现了一些不一样的东西:

博客上的:

const unsigned char gImage_color[27648];

//...........

LCD_DisplayPicture(100, 100, 300, 124,gImage_color);

//............

void LCD_DisplayPicture(uint16_t Xpos, uint16_t Ypos, uint16_t Width, uint16_t Height,const uint8_t *image){

  /* Configure Input Address */
  DMA2D_FG_InitStruct.DMA2D_FGMA = (uint32_t)*image;

//...........
}

官方例程上的:

const uint32_t ARGB4444_150x150[11250];

//............

static void DMA2D_Config(void){

  /* Configure Input Address */
  DMA2D_FG_InitStruct.DMA2D_FGMA = (uint32_t)&ARGB4444_150x150;

  //..........
}

经过对比,我发现对DMA2D_FGMA成员的操作,博客上给的是图片数组的第一个成员,而官方例程给的是图片数组的地址
博客上的做法是封装了一层函数,内聚性好,使用起来比较方便。而我在检查的时候一直没有留意到这一点!!

书到用时方恨少,又开始后悔指针没有认真学。然后我写了个小的测试程序: 指针地址验证

结果发现是因为指向的地址不对- - 把其他地方的数据写到了显存中。

但是很奇怪,这两个地址居然相差不多 。要是相差很多的话就完全显示不出来图片了才对,这样我觉得反而更好排查。结果只差了一点,搞得我老是以为是图片错位、信息头之类的。服了。

将博客例程中的语句修改如下:

 DMA2D_FG_InitStruct.DMA2D_FGMA = (uint32_t)image;

图片可以正常显示。
图标显示 图标显示

开发日志 2019/09/28

实现功能

  • 新增: 触摸图标事件响应
  • 新增: 编写用电器状态转换的宏
    • 采用宏拼接条件运算符的方式实现用电器状态与用电器图标数组名的等价转换
    • 相较于传统的分支语句这种写法使得程序更加简洁可读性更好
  • 改进: 图标结构体增加用电器类型用电器状态用电器编号等成员
  • 改进: 优化并精简触摸画板主程序
  • 改进: 显示多个图标
  • 删除: 触摸画板中关于笔刷、画刷的部分
  • 删除: 触摸画板中关于触摸轨迹显示的部分
    显示四个图标

待完成

  • 字体的显示
    • 大字体字模的制作
    • 文字的刷新(用空格清除原来的文字)
    • 文字空白范围与背景层的混合叠加
  • 前景层与背景层的叠加混合
    • ARGB8888格式的LCD_Layer的配置
    • 透明度混合因子的配置
  • 图片数组的生成
    • RLE编码格式的图片数组解码

今日总结

实用的宏展开

之前编程的时候经常遇到这样的情况:

  • 只有0、1两种状态的分支语句
  • 两种表示同样东西的变量相互转换的麻烦 eg:
int i = 0;
...
if( i == 0){
  ...
} else if ( i == 1){
  ...
}

这种是比较蠢且无脑的写法,下面这种稍好一点:

int i = 0;
...
if( i ){
  ...
} else {
  ...
}

后来我发现就两种情况的话还是条件表达式最方便:

int i = 0;
...
( i ) ? {do something...} : { do another thing...}; 

还有就是更加经常遇到的一种情况:

#define LIGHT 1
#define FAN   2
...
int device = LIGHT; 
int command_light[10];
int command_fan[10];
...
void do_command(int* command_device);

device被改变时,传入do_command的command数组也要求改变。这是就遇到了很纠结的情况:

一种方法是分支判断后,每一个语句块都用不同的参数调用函数;

另一种是引入中间量,每个分支只是将不同的参数给中间量,然后在程序末以中间量作为参数调用函数;

这两种方法对函数来说比较友好,但是对于特殊的执行语句就不太方便了。以本项目为例:

#define DEVICE_STATUS_TOGGLE(device, status) (status) ? (gImage_##device##_off) : (gImage_##device##_on)

...
switch (ptr->device){
  case DEVICE_FAN:
      gImage = DEVICE_STATUS_TOGGLE(fan, ptr->status);
      break;
  case DEVICE_LIGHT:
      gImage = DEVICE_STATUS_TOGGLE(light, ptr->status);
      break;
  ...
}

如果不用宏展开的话还需要在每个case中多加一个判断,着实麻烦。

结构体与函数指针

函数指针的引入让C拥有了类似“接口”一样的特性,使用起来很方便。
同时,在结构体中的函数指针能直接使用结构体中的成员。无需再给定额外的参数(也无法指定)。
对于参数很多的函数来说使用起来很方便。

灵感:图标图片数组作为成员

在整理上述文档的时候,我发现之所以我需要不断地判断用电器类型、用电器状态,是因为各个变量是独立的,没有内聚性!
图标图片数组明明就应该是属于图标的一部分才对,根本没必要把它声明成外部数组,只需要在结构体中增加一个指针成员,然后在初始化图标结构体的时候将对应的数组首地址赋给指针就行了。这样在调用绘图函数的时候就不用再判断了,直接将这个指针作为参数调用绘图函数就行了。
然后我又想到绘图函数也没必要再封装一层了,直接将LCD_DisplayPicture()的内容放到绘图函数里面就行了,坐标什么的都是结构体里的成员,更加方便了。
但是这样又有一个问题,就是不同像素格式的时候切换起来会很麻烦。

叹服:宏定义和预编译if的精妙

如果没有宏定义的话,需要将一个变量声明成外部变量,并且每个用到的文件都要包含该头文件。
而如果不用预编译if直接用普通if来分支的话,程序每次执行都要有一个判断,但是对于配置类的设置来说,这个配置是在程序开始之前就设定好且在程序执行期间不会改动的。但是普通if仍需每次执行都要判断一次,这种地方多了的话不利于程序的高效运行。
使用预编译if的话就不会存在这种问题,在编译期间就将不符合条件的语句块给“删掉”了。在程序执行期间不会再次判断,提升了程序的执行效率。

这种方法适用于配置类的语句块,特别是这种配置的语句穿插在各个不同的文件之中。

开发日志 2019/09/29

推翻:重写绘图函数的想法

之前想过把LCD_DisplayPicture()的内容放到绘图函数Draw_Icon()里面去,可以减少封装的层数。
结果今天一试才发现这样子做反而不好。
主要是因为LCD_DisplayPicture()用到了较多的LCD相关的变量,而这些变量大部分是定义在bsp_lcd.c里面的。虽然可以将这些变量声明成外部变量同时让panel.c包含bsp_lcd.h来将LCD_DisplayPicture()的内容放到绘图函数中,但是这样做增加了程序的耦合性
本来显示图片这种对操作DMA2D的东西就应该放在bsp_lcd.c中,这样程序的内聚性才高。如果把那些头文件、变量都包含进panel.c里去的话就会显得乱。负责某一部分的语句放成一个函数、负责某一块的变量、函数、宏放成一个.c和.h文件,这样才是最合理的设置啊。
虽然之前一直看着别人这么做,但是当自己需要在此基础上增添较多东西或者重写东西的时候就发现很难做好。做不到这么好的内聚和这么低的耦合。其实主要还是对高内聚和低耦合的认知还是不够,没有真正地融入进编程的潜意识中,这着实是需要提高的。

开发日志 2019/10/02

  • 新增:利用字模放大函数显示字符串
  • 新增:PANEL_DEBUG宏
    • 利用可变参数、__LINE__宏、字符串拼接等实现对printf函数的封装,增加了开发效率

遇到问题

显示字符之后无法操作图标

  • 显示字符的函数放在按钮初始化函数之后的话点击触摸屏无反应,之前则可以。

  • 触摸屏的debug信息有正常输出,说明是上层执行函数出了问题

  • 显示字符函数放在图标初始化后点击对应区域没有进入到 图标区域判断的if语句中去,相关代码如下:

/**
 * @brief Touch_Icon_Up 图标被释放时调用的函数,由触摸屏调用
 * 
 * @param x 触摸最后释放时的x坐标
 * @param y 触摸最后释放时的y坐标
 */
void Touch_Icon_Up(uint16_t x, uint16_t y){
    uint8_t i;
	
    PANEL_DEBUG("Funtion Touch_Icon_Up");
	
    for (i = 0; i < ICON_NUM; i++){
        /* 触笔在图标区域释放 */
        if (x <= (icon[i].start_x + ICON_SIZE) && y <= (icon[i].start_y + ICON_SIZE) && y >= icon[i].start_y && x >= icon[i].start_x){
        
            icon[i].touch_flag = 0; /*释放触摸标志*/

            icon[i].status = (icon[i].status == 0) ? 1 : 0; /* 反转用电器状态 */
            
            PANEL_DEBUG("Redraw the icon above");
				
            icon[i].draw_icon(&icon[i]); /*重绘图标*/
            
            PANEL_DEBUG("Redraw the icon below");

            icon[i].icon_command(&icon[i]); /*执行图标的功能命令*/

            break;
        }
    }
}

前后调试信息对比
通过调试信息可以看到虽然触摸屏调用了重绘函数,但是在判断释放区域的时候出了问题,明明是在图标范围内,但是没有进入if。再次回看这个触摸回调函数,是当触摸屏有触摸的时候调用该回调函数,回调函数判断触点是否在图标的区域范围之内,然后执行动作。
灵光一闪:有可能是图标结构体初始化不成功!导致判断图标区域的时候没有符合的值,所以进不到if中去。 解决方法:打印调试日志,看看有没有初始化成功;是否因为第一次显示图标的时候加了delay的原因没有初始化成功?

放大后的字符锯齿严重

野火自带的字库最大只制作了16×24版本的,若要显示更大的字的话有两种方法:

  1. 重新制作大字号的字库
  2. 利用字模放大函数实现动态制作任意大小的字模

我先尝试了第二种方法,利用野火编写的字符放大函数,将原有的16×24的字模放大成64×96的字体后,发现锯齿效果严重
放大字模锯齿严重
用第一种方法——重新制作字模的话效果应该会更好。

硬错误的出现

当触摸原点位置一个图标的范围内时(图标大小为96×96,从原点算起触摸范围为(96, 96)),会出现硬错误(HardFault)
触摸原点位置出现硬错误
似乎验证了前面的猜想:初始化不成功,导致坐标为0!当触摸回调出发的时候调用了空的、未知的绘制函数,导致出现了硬错误!
为什么初始化不成功?会不会是调用放大函数的时候,声明的用于存储放大后字模的数组覆盖掉了初始化的数据?但又感觉不太可能。。。果然还是对内存的理解不够深入和透彻,才疏学浅。。。

烧录程序后一段时间内闪屏严重

现象:

  • 每次烧录程序后的一段时间内出现闪频
  • 一段时间过后闪屏现象消失(这一段时间内无任何操作)
  • 不闪屏之后按复位键,重启后不闪屏
  • 不闪屏之后断电源,重新上电后不闪屏

分析:
闪屏现象据手册来讲一般是像素时钟频率设置过高,导致LTDC输出的帧像素数据的频率跟不上LCD刷新显示的频率,当LCD准备显示下一帧而读取引脚准备接收帧像素数据的时候,LTDC因为跟不上LCD频率而没有输出帧像素数据,导致LCD读到的是空值!所以屏幕会先暗下去一会儿,当下一次LCD下一次读取帧像素数据的时候LTDC跟上了,原先暗下去一会儿的地方继续显示图标,两种情况来回交替导致出现“闪屏”的现象。
而至于为什么屏幕的其他地方没有明显的闪屏现象,只有图标的部分闪屏明显,我猜是因为其他地方显示的是白色,而背光灯就是白色的,当没有像素数据输入的时候就显示背光的颜色,所以我们看起来其他地方没有闪屏(一直是白色)实际上是白色像素-白色背光灯的来回交替,其他地方也是在闪屏的,只是错觉导致我以为没有闪屏而已。图标部分因为本来就是暗颜色的,暗颜色和白色背光交替就产生了我们看到的闪屏现象。
对于为什么一段时间后就稳定的情况至今无解。一开始我以为是液晶屏的液晶需要一段时间的持续电压让它保持反射特定方向的光,后来觉得应该不是,因为在闪屏期间操作图标它是可以迅速切换开关状态图标的,只不过依然在闪屏而已。如果是因为需要持续电压的话应该根本不会迅速切换开关状态图标才对。所以我百思不得其解。

开发日志 2019/10/03

LCD显示质量的提高

之前emwin版本的室内控制面板的整体色调偏白,我以为是LCD屏幕性能限制的原因。后来我把同样的图标用LTDC显示出来,发现LCD上显示的图标并不那么偏白,很接近电脑显示的颜色。说明并不是LCD本身性能的限制,而是软件的问题。
猜测了一下可能的原因,大概是因为LCD Layer 1 和 Layer 2 的混合没有设置好,导致第二层(前景层)设置成了微透明的白色,导致经过层混合之后整体画面偏白;另一种可能是位图转换工具BmpCvt转换的RLE编码的图标图片数组在转换的时候出现偏差,导致显示的时候整体偏白。

触摸失效测试

对之前所猜测的图标结构体初始化不成功的问题做进一步测试。
这是所添加的测试代码:

 for (i = 0; i < ICON_NUM; i++){

    PANEL_DEBUG("icon no = %d, x = %d, y = %d",i,icon[i].start_x,icon[i].start_y);
        
    if (触笔在图标区域释放){
       ...
    }
 }

这里主要是看看触摸屏在调用图标绘制函数时各个图标的坐标是否正常。
用正常状态(显示字符语句放在图标结构体初始化之前)作为测试对照组,其不同状态下的输出语句如下:

  • 触摸一号图标:
    无失灵触摸一号图标
  • 触摸四号图标:
    无失灵触摸四号图标
  • 触摸原点范围内的图标:
    无失灵触摸原点

接下来是触摸失灵的测试实验组,其不同状态下的输出语句如下:

  • 触摸一号图标:
    有失灵触摸一号图标
  • 触摸原点范围内的图标:
    有失灵触摸原点

从触摸一号图标的输出语句中可以看出,图标的坐标异常。说明跟预想的一样,初始化不成功。
但是实际上,图标在LCD上显示的位置还是跟预期的一样 ,并没有移动到原点。说明最开始的时候,图标结构体是初始化成功的,成功地被写入到显存相应的位置。但是在调用显示字符串语句之后,原有的图标结构体被改写了,图标的坐标异常,触摸回调函数判断不成功,无法重绘按钮,导致出现了“触摸失灵”的现象。

疑问:是否因为放大显示字模函数中的某些语句导致结构体被重写?如果不用放大函数直接用原有的16×24字模显示会不会出现这种情况?

字模显示

利用正常的字符串显示函数触摸屏反应正常。 测试代码如下:

  LCD_DisplayStringLineEx(0,0,64,96,"28%",0);

  Touch_Icon_Init();

  for (i = 0; i < ICON_NUM; i++)
  {
   /* 描绘图标 */
  }
  LCD_SetFont(&Font16x24);
  LCD_DisplayStringLine(LCD_LINE_7,"29");
  

显示结果如下:
正常字符显示

触摸屏串口信息如下:
无失灵触摸一号图标

触摸反应正常。没有去细究具体问题在哪个地方,开始着手大字号字模的制作。

开发日志 2019/10/04

字模软件的使用

1.字体的选择

注意:在选择中文字体的时候要注意数字、字母和符号的宽度
因为中文字是两个字符宽的,而英文是一个字符宽的。当用某一些中文字体显示数字、字母和符号的时候有可能会出现显示不全的情况。如图所示:

中文字体“方正兰亭特黑简体”字模显示结果:
中文字体显示不全

英文字体字模显示结果:
英文字体显示完全

可以注意到,中文字体的宽高比为1:1 ,而英文字体的宽高比为1:2 。而对于常用的ASCII字符而言,1:2的宽高比完全足够了。
对于我们目前的项目需求而言,我们的字模需要满足以下要求:

  • 相对面板来说要足够大
  • 清晰易读
  • 只用到数字和一些表示单位的字母、符号

所以我们选择的字体要够粗,这样别人在离得比较远的时候才能清楚地看见面板上的字;没有用到中文字符,所以直接选择1:2的宽高比即可。

2.设置所需要的字号

设置字号

注意:设置完了之后要修改点阵大小!!

生成的字库数组是按照点阵的宽高来生成的,只会生成黄色方框内的字模内容。
我们可以看到明明设置好了字的宽高,但是黄色方框内却没有内容。原因就是没有根据字的宽高修改点阵的大小(即黄方框的大小),导致16×16的点阵显示不出96×96的字。

设置点阵大小

可以看到,将点阵修改为与字体宽高一致后字符可以完全显示。

3.设置输出配置

字模选项

字模选项中的参数可以查看野火资料中A盘(资料盘)\4-开发软件\字模软件\PCtoLCD\字库工具下的readme2002.txt ,里面有较为详细的介绍。
其中,值得注意的是每行显示数据点阵的设置。这里面的点阵数量是指生成的txt文件中每行十六进制数的个数,跟数组的排版有关。下图为点阵数量为12时生成的字模数据:

以一行48个像素为例,一位表示一个像素,一个十六进制数表示2 × 8 个像素点,所以六个十进制数表示字模的一行。我们设置的是输出文件一行12个十六进制数,所以输出文件中的一行表示两行字模。
注意:我们最后是将文件输出文件的内容存进一维数组中,所以无论怎么设置输出文件一行的个数都对程序无影响!主要是跟排版有关。要理清思路。

4.生成字库

点击打开文本文件即可载入批量的文本,其中目录下的ascllCODE.TXT 为32至126位的ASCII码文件。
在输出文件中取消勾选“生成二进制字库文件”,然后点击开始生成,即可生成txt格式的十六进制文件了。

开发日志 2019/10/05

字模数组的研究

野火自带的显示函数是根据3个自带的字库来编写的,并不适用于大号的字模。因为自带的字库是一个数组位表示一行字模数据!!其最宽的数据也就16,用两个字节的十六进制数(如0x1000)就可以表示。12x12的字库是用了两字节十六进制数的高12位,低4位不用。
而我们自己生成的字库是以一个字节的十六进制数生成的!并没有一个数组位表示一行字模数据!!按照我们所需的48x96的字模,12个数组为才表示一行数据!所以当用到野火编写的LCD_DisplayChar()时,就出现了字模数组定位不准确的情况!!导致显示不出来正确的字符。

LCD_DrawChar函数分析

最主要的是这一条语句:

if((((c[index] & ((0x80 << ((LCD_Currentfonts->Width / 12 ) * 8 ) ) >> counter)) == 0x00) &&(LCD_Currentfonts->Width <= 12))||(((c[index] & (0x1 << counter)) == 0x00)&&(LCD_Currentfonts->Width > 12 )))

如此丧心病狂的语句简直了!但还是得慢慢分析。为了分析这个语句还专门画了个分解图:

分解图不够,再来流程图:

// 有待补充的解释

如果要显示我们这个数组的话,得用width / 8 表示数组下标,witdth % 8 表示一个下标中的位移数。

开发日志 2019/10/08

字模的错位显示


原因:生成的字模是向前扫描的,而显示的时候是按位左移然后写进显存的,这就导致了左右颠倒的情况。而因为每次左移是以一个字节为单位的,所以每8个像素点左右颠倒一次,导致出现了这种字符分段错位的情况。
至于生成字模时扫描的先后、检测字模时的左移右移、显存写入的先后之间的对应关系还有待琢磨和研究。

将代码中的左移改成右移,可以正常显示:

开发日志 2019/10/10

面板交互原型模型

利用Axure原型软件设计面板交互原型如下:

面板上的未完成版:

面板上初步实现界面如下:

其中我发现出现了几个问题:之前我以为图标结构体中的 end_xend_y 成员是没用的,因为最开始的时候只有图标一种元素,这样触摸屏在检测范围的时候直接用start_x + ICON_SIZE 来检测即可。结果因为后面拓展了不同大小的 的图标,导致触摸范围发生了改变,检测范围也需要改变。所以仍然需要增加 end_xend_y 成员,通过末坐标与起始坐标之间的范围来判断。或者加入widthheight 成员,原理上是一样的。 还有就是得想办法简化初始化的程序,不然图标一多,就一大片的初始化结构体成员的语句,很冗余。
还有就是触摸屏回调函数中,每次触摸都反转状态不适用于菜单栏的控制。因为菜单栏是你点一次再点击都一直是选中状态,并且其他菜单栏要被置为非选中的状态,这才是菜单栏的交互逻辑。

开发日志 2019/10/11

完成菜单栏控件的开发

主要逻辑是点击一个按钮后,将该标签状态置为选中,并将另外两个标签置为未选中。

按钮的逻辑则是被点击时判断当前按钮状态,并切换成相反状态

其中主要的难点是被点击的标签与另外两个标签的交互。传进控制函数的是一个图标结构体指针,事先并不知道它的下标;并且我们并不知道事先哪一个位置的标签会被点击,这就导致了我们没法确定是使用当前下标的+1、+2、还是-1的方式访问另外两个标签。

一开始我是用的取余法去访问另外两个页签,分别用用当前页签下标+1、+2并取余来访问另外页签,类似于循环队列的思想。

...
mune_icon[(i + 1) % 3].status = 0;
mune_icon[(i + 2) % 3].status = 0;
...

我先是用当前被点击的页签的起始位置与另外三个页签的起始位置进行比较,以得出当前被点击的页签的下标;然后利用上面的语句将另外两个页签的状态置零,然后再将被点击的页签的状态置一。

然后我发现根本没必要那么麻烦- - ,只要先将所有的页签状态置零,然后再将当前页签置一就可以。之前定势思维地认为不能改变当前页签的状态,只能讲它置一。后来发现根本没必要,先置零再置一的效果是完全一样的。

上述的方法适用于多个环状 的标签页:当一个页签被点击然后需要相邻的两个页签状态改变的时候就可以用上述算法:

meun_icon[(current_index + 1) % menu_num ]... // 当前页签的后一个页签
meun_icon[(current_index + menu_num - 1) % menu_num ]... // 当前页签的前一个页签

对于触控事件的理解

触控事件分成几种情况

触控标志 \ 触控类型 触摸 释放
0 进入 点击
1 移出 点击

对于使用鼠标来交互的界面,这个移入移出 效果有很好的引导效果。当用户鼠标移入时按钮变色,引导用户点击按钮;点击后再变色,给用户点击到了的反馈。
而面板这种触屏类的交互这种引导性不够,大多数是用动态的箭头来引导用户滑动的,或者是按钮发光之类。不过也可以当用户按住的时候绘制一个矩形或圆形,表明该按钮被选中了。

开发日志 2019/10/12

对于绘制函数和命令函数的理解

绘制函数是根据当前图标结构体的参数来绘制出相应图标的。
命令函数是该图标被点击之后,需要执行的相关动作!一般是用来改变图标参数、切换界面状态等等。

页面切换:触屏函数的重构

加入了菜单栏切换页面后,我发现了之前一直没有发现的问题:在不同的页面,各个图标在显示屏上是有重合的。而最开始的的触屏回调仅仅是通过判断触点坐标是否在图标的范围之内。
这样的话在多界面中就会出现问题:如果多个界面的图标位置重合,该如何区分是哪个界面的图标被点击了?
这样就需要用到界面判断,然后根据不同的界面将触点坐标给不同图标去执行。

开发日志 2019/10/17

  • 完成: 触控处理逻辑框架。
  • 优化: 利用 Widget_TouchUpHandler 对处于不同界面下的触点进行处理,提高代码复用性和程序可读性。
  • 优化: 利用 Icon_Struct_Init 初始化结构体,减少了每个数组成员中相同结构体成员的赋值的重复代码,优化程序结构
  • 优化: 利用 Matrix_Init 初始化一组图标的初始位置,提供可修改的水平图标数量垂直图标数量水平间距垂直间距 四个参数,使得可以只修改一个参数而改变一整组图标之间的排列情况,方面后期修改和拓展。
  • 待做: 屏幕显示字符串函数
  • 待做: 串口信息接收处理

面板实际操作演示

开发日志 2019/10/26

重学UART的传输原理

最开始实现面板功能的时候曾经出现过一个当时觉得很奇怪的问题:面板串口接收到的命令时好时坏,并且前后测试的结果相互矛盾。经分析后发现是因为8266按下Reset时串口会发出一个‘00’字符,由于该字符的存在,导致面板接收到的指令串产生了位移,造成指令串的解析错误,所以面板功能时好时坏。
而我当时觉得很奇怪,为什么会产生这么一个位移。因为理论上来说间隔这么长时间的串口信息不应该被识别成同一个指令串才对。询问原面板开发人员后才知道原来他是将面板串口接收到的字符存在一个数组中,每接收一个字符下标自增1,满一个指令串的长度就执行命令。
我又一次疑惑:为什么要一个个接收?不能一次性全部一起接收吗?后来一分析如果全部一起接收的话是需要利用流控引脚的(有待补充)
之前尝试着直接不用F429直接将ST跟AP的串口接起来想试一下优化过后的代码正不正确,结果发现传上去的数据全是0。我一开始以为是我代码有哪些部分删错了,过了很久之后我重新回来检查这个问题,利用串口经过分段测试之后发现并没有什么问题。结果直接将两个8266的串口互连的时候就发现了:接收端接收到一个字符进一次接收处理函数,而不是接收完一整个指令串之后才进入接收处理函数。
我在发送端的编程是一次发送一整个指令串的,理论上来说应该不会出现这种情况才对。
同时,我用电脑串口助手给接收端发送指令串,是可以正常读取指令串的;面板给接收端发信息也没问题。 所以关键的问题就在于8266的串口发送函数。

关于STM32429串口外设的TC标志位

一开始我以为这种单个传输和整串传输是因为UART外设中的TC标志位的原因,然后就去详细研究F429官方参考手册中关于UART的部分。比较有用的是这副图:(截取自STM32F4xx Reference Manual.pdf P972)
由图可知:串口先等待TXE标志置零,然后开始将数据先传输到DR数据寄存器,等TXE置一之后才将数据寄存器的值输出到TX。如此完成多回之后,等DR里面的数据全部被TX传输出去之后,TC就会被硬件置一。

注意:图中右下角的TC is not set because TXE=1 有误,应为TC is set because TXE=1

官方的编程步骤中提到:

...
7. 在 USART_DR 寄存器中写入要发送的数据(该操作将清零 TXE 位)。为每个要在单缓 冲区模式下发送的数据重复这一步骤。
8. 向 USART_DR 寄存器写入最后一个数据后,等待至 TC=1。这表明最后一个帧的传送已完成。禁止 USART 或进入暂停模式时需要此步骤,以避免损坏最后一次发送。

这说明这个TC标志位是给软件来处理的,硬件中的TX仍是以单个字节的形式发送的,与TC无关。
疑问:既然串口的通讯协议是一次发送8个数据位置(1字节),那么必然每次发送都是单个字节发送的,那么8266中是怎么一次读取一整个指令串的?他是怎么认为分别发送的单个字符是连起来的字符串的?这就需要我去查阅8266中关于UART函数的具体实现了。这又是一个不小的工程。

研究的原因

其实主要是觉得这种分开接收太麻烦了,想着能不能有所简化。原开发者写的串口中断处理函数十分粗糙,虽然目前能用,但是还是应该去规范化。

待完成的畅想

时序图的补充:类似业务流程图,不过更加强调的是同一时刻各个进程之间的相互配合。更多的是强调“同时”;而业务流程图更强调“先后”。时序图由于跟时钟信号紧密相关,更能体现同一时刻的关系。
思考:能不能将各个8266和面板之间的相互配合关系画成时序图的形式?

框图的补充:框图是把一个个寄存器以矩形容器的形式画出来,矩形容器里划分出各个标志位,各个寄存器中不同的标志位相互配合,用箭头的形式汇总成一个描述功能的框。同时引入总线、时钟以及其他外设,展现该外设主要功能的及其实现逻辑的图。 思考:我们的项目能不能也以这种形式画出来?各个模块、模块中包含的主要处理文件、文件中的主要处理函数,最后汇总起来,实现某的大的功能。

TODO:重新审视原面板功能及实现逻辑