STM32 U8G2基础动画效果实现

前言:

本文基于u8g2绘图库,用C语言实现了部分基础的动画效果

使用硬件为stm32f103zet6,即正点原子精英开发版;和0.96寸oled屏iic驱动

使用该动画库前,需要读者自行移植u8g2,并配置底层驱动(通信、延时等),为了实现高帧率,推荐采取SPI+DMA的方式驱动屏幕

此外,由于本库只进行了基础的性能优化,对于一些硬件较差的平台,可能效率不佳;代码有不足之处,还请大佬们多多指教

效果展示:

动图中,依次调用了 逐字打印、收缩清屏、闪烁、发散显示、滑动字符串 和 缓动函数控制图形实现弹跳效果。

代码:

1.逐字打印字符串

/*
动效 - 逐字符显示字符串
参数:   x/y - 打印位置左下角坐标
        *text - 要打印的字符
        space - 字符间隔
        delay - 打印时间间隔
tip:需要在函数外部设置字体
*/
void PrintStr_CharByChar(u8g2_t *u8g2, uint8_t x, uint8_t y, uint8_t* text,uint8_t space, uint16_t delayMS){
  uint8_t text_len;

  text_len = strlen(text);
  for(uint8_t i = 0; i < text_len; i++){
    u8g2_DrawGlyph(u8g2, x + space * i, y, text[i]);
    u8g2_SendBuffer(u8g2);
    HAL_Delay(delayMS); //这个延时要短一些才有流畅的转换效果
  }
}

很基础的动效,需要在调用前先设置气体,space间隔的选取需要考虑到使用的字体宽度。需要注意的是,u8g2的字体库中,并不是 所有的字体 中的字母 都等宽。

调用格式如下:

u8g2_SetFont(&my_u8g2, u8g2_font_8x13B_tr);
PrintStr_CharByChar(&my_u8g2, 10, 10, "test string", 8, 100);

2.滑动清屏

typedef enum{
  CLEAR_LEFT,
  CLEAR_RIGHT,
  CLEAR_UP,
  CLEAR_DOWN
} LinearClearDirction;

void LinearClearScreen(u8g2_t *u8g2, LinearClearDirction direction, uint8_t speed, uint16_t delayMS){
  int screenWidth = u8g2_GetDisplayWidth(u8g2);
  int screenHeight = u8g2_GetDisplayHeight(u8g2);
  int x, y;
  
  u8g2_SetDrawColor(u8g2, 0); //设置绘图颜色为黑色
  
  switch (direction) {
    case CLEAR_LEFT:
      for (x = 0; x < screenWidth; x += speed) {
        u8g2_DrawBox(u8g2, 0, 0, x, screenHeight);
        u8g2_SendBuffer(u8g2);
        HAL_Delay(delayMS);
      }
    break;
      
    case CLEAR_RIGHT:
      for (x = screenWidth - 1; x >= 0; x -= speed) {
        u8g2_DrawBox(u8g2, x, 0, screenWidth, screenHeight);
        u8g2_SendBuffer(u8g2);
        HAL_Delay(delayMS);
      }
    break;

    case CLEAR_UP:
      for (y = 0; y < screenHeight; y += speed) {
        u8g2_DrawBox(u8g2, 0, 0, screenWidth, y);
        u8g2_SendBuffer(u8g2);
        HAL_Delay(delayMS);
    }
    break;

    case CLEAR_DOWN:
      for (y = screenHeight - 1; y >= 0; y -= speed) {
        u8g2_DrawBox(u8g2, 0, y, screenWidth, screenHeight);
        u8g2_SendBuffer(u8g2);
        HAL_Delay(delayMS);
      }
      break;
  }
  u8g2_ClearBuffer(u8g2);
  u8g2_SendBuffer(u8g2);
  // 恢复正常绘图模式
  u8g2_SetDrawColor(u8g2, 1);
}

调用动画前,需要缓冲区存在待清屏的图像。效果就是屏幕一边缘,向另一层逐渐清除屏幕内容

PrintStr_CharByChar(&my_u8g2, 10, 10, "test string", 8, 100);    //清屏对象
LinearClearScreen(&my_u8g2, CLEAR_UP, 2, 10);

3.闪烁

typedef void (*DrawFuncCallback)(u8g2_t *u8g2, uint8_t x, uint8_t y, void* params); //带传参的句柄

//画实心矩形
void EasingDrawBox(u8g2_t *u8g2, uint8_t x, uint8_t y, void *params){
  uint8_t* box_params = (uint8_t*)params;
  uint8_t width = box_params[0];
  uint8_t height = box_params[1];
  
  u8g2_DrawBox(u8g2, x, y, width, height);
}

void BlinkDraw(u8g2_t *u8g2, DrawFuncCallback drawFunc, uint8_t x, uint8_t y, void *params, uint8_t times, uint16_t delayMS) {
    for (uint8_t i = 0; i < times; i++) {
        // 反色绘制
        u8g2_SetDrawColor(u8g2, 2); // 设置为反色模式
        drawFunc(u8g2, x, y, params);
        u8g2_SendBuffer(u8g2);
        // 等待
        HAL_Delay(delayMS);
        // 再次反色绘制(恢复原状态)
        drawFunc(u8g2, x, y, params);
        u8g2_SendBuffer(u8g2);
        // 等待
        HAL_Delay(delayMS);
    }
    // 恢复正常绘图模式
    u8g2_SetDrawColor(u8g2, 1);
}

一个比较复杂的动效,按照句柄格式,传入一个封装的绘图函数,这里绘图坐标x/y是固定uint8_t类型,但void *param根据传入的绘图函数不同,其类型和内容都会有所区别;传参时,应该严格参照封装后的绘图函数(以EasingDraw开头)内的解包格式

这里调用了绘制矩形的函数,因此param只是一个数组指针;效果是一个矩形间隔100ms闪烁共5次

uint8_t draw_param[2] = {100, 50};
BlinkDraw(&my_u8g2, EasingDrawBox, 20, 20, draw_param, 5, 100);

4.收缩清屏

/*
动效 - 收缩清屏(黑色绘制法)
参数:   speed - 收缩倍率
        delay - 收缩的延时
tip:动效会在完成时清空缓冲区
*/
void ShrinkClearScreen(u8g2_t *u8g2, uint8_t speed, uint16_t delayMS){
  int screenWidth = u8g2_GetDisplayWidth(u8g2);
  int screenHeight = u8g2_GetDisplayHeight(u8g2);
  int x = 0, y = 0, width = screenWidth, height = screenHeight;
  
  u8g2_SetDrawColor(u8g2, 0); //设置绘图颜色为黑色
  while(width > 0 && height > 0){
    // 清除顶部区域
    if (y > 0) {
     u8g2_DrawBox(u8g2, 0, 0, screenWidth, y);
    }
    // 清除底部区域
    if (y + height < screenHeight) {
      u8g2_DrawBox(u8g2, 0, y + height, screenWidth, screenHeight - (y + height));
    }
    // 清除左侧区域
    if (x > 0) {
      u8g2_DrawBox(u8g2, 0, y, x, height);
    }
    // 清除右侧区域
    if (x + width < screenWidth) {
      u8g2_DrawBox(u8g2, x + width, y, screenWidth - (x + width), height);
    }
    u8g2_SendBuffer(u8g2); // 发送绘制命令到屏幕

    // 更新矩形位置和大小
    x += (2 * speed);
    y += (1 * speed);
    width -= (4 * speed);
    height -= (2 * speed);
    
    HAL_Delay(delayMS);
  }
  u8g2_ClearBuffer(u8g2);
  u8g2_SendBuffer(u8g2);
  // 恢复正常绘图模式
  u8g2_SetDrawColor(u8g2, 1);
}

从屏幕边缘开始,向屏幕中心点以矩形形状收缩,并不断清屏;调用方式同滑动清屏

5.旋转清屏

float tan_table[360] = {1}; //设置第一个元素为1,表示表尚未初始化

typedef enum{
  CLEAR_ONE = 360,    //一条线清屏360度
  CLEAR_TWO = 180,    //两条线各180
  CLEAR_FOUR = 90    //四条线各90
} SpinClearDirction;

/*
动效 - 旋转清屏(点扫描+查表法)
参数: direction - 方向参数,ONE同时只有一条射线扫描清屏,TWO两条,FOUR四条
      speed - 收缩倍率,speed应该是90的因数!
      delay - 收缩的延时
tip:动效会在完成时清空缓冲区
*/
void Init_tantable(void){
  for (int i = 0; i < 360; i++) {
        tan_table[i] = tan(i * M_PI / 180.0);
  }
}

// 判断点是否在需要清除的区域内
int is_in_clear_area(SpinClearDirction direction, int x, int y, int centerX, int centerY, int angle, int sector) {
  int dx = x - centerX;
  int dy = y - centerY;
  //原点处理
  if(dx == 0 && dy == 0) return 1;
  //特殊角度处理
  switch (angle){
        case 0:   if (dy == 0 && dx > 0) return 1;
                  break;
        case 90:  if (dx == 0 && dy > 0) return 1;
                  break;
        case 180: if (dy == 0 && dx < 0) return 1;
                  break;
        case 270: if (dx == 0 && dy < 0) return 1;
                  break;
        default:  break;
  }
  //分象限处理
  float tan_point = (float)dy / (float)dx;
  
  switch(direction){
    case CLEAR_ONE:{
      if(tan_point <= tan_table[angle]){
        switch(sector){
          case 0: if(dx > 0 && dy > 0) return 1;
                  break;
          case 1: if(dx < 0 && dy > 0) return 1;
                  break;
          case 2: if(dx < 0 && dy < 0) return 1;
                  break;
          case 3: if(dx > 0 && dy < 0) return 1;
                  break;
          default: break;
        }
      }
      break;
    }
    case CLEAR_TWO:{
      if(angle > 0 && tan_point <= tan_table[angle]){  //防止angle = 0时,tan90 = inf影响判断
        if(tan_table[angle] > 0 && tan_point > 0) return 1;
        if(tan_table[angle] < 0 && tan_point < 0) return 1;
      }
      break;
    }
    case CLEAR_FOUR:{
      if(tan_point > 0){
        if(tan_point <= tan_table[angle]) return 1;
      }else if(tan_point < 0 && angle != 0){  //防止angle = 0时,tan90 = inf影响判断
        if(tan_point <= tan_table[angle + 90]) return 1;
      }
      break;
    }
  }
  return 0;
}

// 清除区域函数
void clear_sector(u8g2_t *u8g2, SpinClearDirction direction, int width, int height, int centerX, int centerY, int angle, int sector) {
  for (int x = 0; x < width; x++) {
    for (int y = 0; y < height; y++) {
      if (is_in_clear_area(direction, x, y, centerX, centerY, angle, sector))
        u8g2_DrawPixel(u8g2, x, y);
    }
  }
}

void SpinClearScreen(u8g2_t *u8g2, SpinClearDirction direction, uint8_t speed, uint16_t delayMS){
  int width = u8g2_GetDisplayWidth(u8g2);
  int height = u8g2_GetDisplayHeight(u8g2);
  int centerX = width / 2, centerY = height / 2;
  int sector; //象限
  
  u8g2_SetDrawColor(u8g2, 0); //设置绘图颜色为黑色
  
  if(tan_table[0] == 1) Init_tantable();  //如果未初始化表,初始化
  
  for(int angle = 0; angle < direction; angle += speed){
    sector = (float)angle / 90.1;  //将90°分给前一象限
    clear_sector(u8g2, direction, width, height, centerX, centerY, angle, sector);
    u8g2_SendBuffer(u8g2);
    HAL_Delay(delayMS);
  }
  u8g2_ClearBuffer(u8g2);
  u8g2_SendBuffer(u8g2);
  
  u8g2_SetDrawColor(u8g2, 1);
}

从屏幕中心做射线,射线旋转并不断清屏,提供了三种子方式,分别是1条清360度、2条各清180、4条各清90度,可以自行尝试

具体实现上,尽管使用了查表法,但是速度还是较慢,如果想要流畅的动画效果,建议延时设为0

6.进度条

/*
动效 - 进度条
参数: x/y - 进度条左上角坐标
      width/height - 进度条长宽
      percentage - 进度,按百分比输入
      reuse - 重新调用标志位,0-第一次调用,1-重复调用
tip:如果是重复调用,置位reuse后,x/y/width/height参数都可以随意输入,函数会调用此前的参数
*/
void ProcessBar(u8g2_t *u8g2, uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint8_t percentage, uint8_t reuse){
  static uint8_t pre_x, pre_y, pre_w, pre_h;
  //如果是重复调用,则不再绘画进度条外框
  if(!reuse){
    u8g2_DrawFrame(u8g2, x, y, width, height);
    pre_x = x, pre_y = y, pre_w = width, pre_h = height;
  }
  //绘制填充的部分
  uint8_t filledWidth = (pre_w - 2) * percentage / 100;
  u8g2_DrawBox(u8g2, pre_x+1, pre_y+1, filledWidth, pre_h-2);
  u8g2_SendBuffer(u8g2);
}

动效本身只画出特定百分比完成度的进度条,想要运动效果需要外层进行驱动

第一次调用后就会存储进度条尺寸参数,因此后续调用时,在置位reuse后,就可以只输入当前进度percentage

ProcessBar(&my_u8g2, 10, 10, 100, 10, 0, 0); //初次调用
for(int i = 0; i < 100; i++){
    ProcessBar(&my_u8g2, 66, 66, 66, 66, i, 1); //重复调用
}

7.发散显示

void SpreadShow(u8g2_t *u8g2, DrawFuncCallback drawFunc, uint8_t drawX, uint8_t drawY, void *params, uint8_t speed, uint16_t delayMS){
  int screen_width = u8g2_GetDisplayWidth(u8g2);
  int screen_height = u8g2_GetDisplayHeight(u8g2);
  
  int centerX = screen_width / 2, centerY = screen_height / 2;
  int clip_width = 0, clip_height = 0;
  
  while(clip_width < screen_width && clip_height < screen_height){
    int x0 = centerX - clip_width/2;
    int y0 = centerY - clip_height/2;
    int x1 = centerX + clip_width/2;
    int y1 = centerY + clip_height/2;
    
    // 防止限制区超出屏幕边界
    if(x0 < 0) x0 = 0;
    if(y0 < 0) y0 = 0;
    if(x1 > screen_width - 1) x1 = screen_width - 1;
    if(y1 > screen_height - 1) y1 = screen_height - 1;
    
    u8g2_ClearBuffer(u8g2);
    u8g2_SetClipWindow(u8g2, x0, y0, x1, y1);
    drawFunc(u8g2, drawX, drawY, params);
    u8g2_SendBuffer(u8g2);
    
    clip_width += 2*speed;
    clip_height += speed;
    
    HAL_Delay(delayMS);
  }
  
  u8g2_SetMaxClipWindow(u8g2);
}

收缩清屏的反向版,但由于需要不断进行重绘,运行速度比较下会更慢

  uint8_t draw_param[2] = {100, 50};
  SpreadShow(&my_u8g2, EasingDrawBox, 5, 5, draw_param, 1, 0);

8.缓动函数控制图形运动

最复杂的一个函数,由于开发的不是特别深入,在缓动函数的参数意义上有些不明确;如果使用时出现问题,可以留言提问,或者参考其他大佬开发的代码

先看顶层封装

typedef struct{
  float t;  //时间
  float b;  //开始位置,即t=0时函数值
  float c;  //变化量,即系数
  float f;  //动画单位时间内帧数
} EasingFuncParams;

//句柄类型定义
typedef void (*DrawFuncCallback)(u8g2_t *u8g2, uint8_t x, uint8_t y, void* params); //带传参的句柄
typedef float (*EasingFuncCallback)(EasingFuncParams* params); //缓动函数句柄

//结构体
typedef struct {
    EasingFuncCallback easingfuncX; //x轴调用的缓动函数
    EasingFuncCallback easingfuncY; //y轴调用的缓动函数
    EasingFuncParams* paramX;
    EasingFuncParams* paramY;
    DrawFuncCallback drawFunc;  //绘图函数
    uint8_t x;  //起始坐标
    uint8_t y;
    void *params; //绘图函数剩余参数
    uint8_t clear;  //画一帧后清屏,0-否,1-是
    uint16_t frame_cnt;  //绘画总帧数
} EasingFuncDrawParams;
/*
动效 - 缓动函数显示
参数: drawparams - 结构体储存的参数
      last_pos - uint8_t型指针,绘图最后的位置会在函数调用结束时保存到该地址
      delayMS - 延时
*/

void EasingFuncDraw(u8g2_t *u8g2, EasingFuncDrawParams *drawparams, uint8_t* last_pos, uint16_t delayMS){
  int x_t = 0, y_t = 0, i = 0;  
  
  for(i = 0; i < drawparams->frame_cnt; i++){
    drawparams->paramX->t = i;
    drawparams->paramY->t = i;
    
    x_t = (int)(drawparams->easingfuncX(drawparams->paramX));
    y_t = (int)(drawparams->easingfuncY(drawparams->paramY));
    
    if(drawparams->clear) u8g2_ClearBuffer(u8g2);
    drawparams->drawFunc(u8g2, drawparams->x + x_t, drawparams->y + y_t, drawparams->params);
    u8g2_SendBuffer(u8g2);
    HAL_Delay(delayMS);
  }
  
  last_pos[0] = drawparams->x + x_t;
  last_pos[1] = drawparams->y + y_t;
}

缓动函数的概念可以自行了解,总之,调用时需要:

传入一个绘图函数,这是要控制的目标;

为x/y方向各传入一个缓动函数,实现具体运动;

传入其他参数,如总帧数,单位时间帧数等

/*线性加速,t>0*/
float Linear(EasingFuncParams* params){
  return params->c * params->t / params->f + params->b;
}
/*平方减速,0<t<1,f应等于frame_cnt,c=位移距离*/
float QuadraticEaseOut(EasingFuncParams* params){
  float t = params->t;
  
  t /= params->f;
  return - params->c *t*(t-2) + params->b;
}

上面是两个缓动函数,他们的使用有所不同

这里t是迭代用的时间参数,f是单位时间内的帧数。理论上,t/f的范围应该在[0,1],但有些缓动函数,超出1后仍然可以使用,如上面的线性缓动函数;而第二个平方减速函数,则不可以,它的系数c等于它[0,1]间位移的距离,b等于起始值,一般置零

为了概念统一,可以如此设置参数:

缓动函数 - c设置为预想的位移距离,b置零,f设置与frame_cnt相同;

顶层函数 - frame_cnt 设置为预期动画总帧数

最后是调用,调用流程比较复杂,请严格按照例程执行

uint8_t draw_param[3] = {5, 5, U8G2_DRAW_ALL};  //绘图函数的额外参数,由于调用的是椭圆绘制函数,因此额外参数有3个
uint8_t last_pos[2];  //存储最后一次画图坐标
//x轴缓动函数参数
EasingFuncParams paramsx = {
  .t = 0,
  .b = 50,
  .c = 0.5,
  .f = 0
};
//y轴缓动函数参数
EasingFuncParams paramsy = {
  .t = 0,
  .b = 0,
  .c = 1,
  .f = 0
};
//初始化缓动绘图函数参数
EasingFuncDrawParams mydrawbox= {
      .easingfuncX = Linear,  //x轴调用线性缓动
      .easingfuncY = CubicEaseIn, //y轴调用立方加速缓动
      .paramX = &paramsx, //为两轴函数赋参数
      .paramY = &paramsy,
      .drawFunc = EasingDrawEllipse,  //调用椭圆绘图函数
      .x = 10,  //起始坐标(10, 10)
      .y = 10,
      .params = draw_param, // 指向实际参数的指针
      .clear = 1, //绘制一个后清除上一个
      .frame_cnt = 50  //总共绘制50帧
  };
  
EasingFuncDraw(&my_u8g2, &mydrawbox, last_pos, 0);

源代码可以在这里获取

百度网盘 请输入提取码

提取码etpt

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: STM32是一种微控制器型号,它可以通过SPI总线和其他器件进行通信。U8g2是一种可以在STM32上使用的图形库,它可以让开发者很方便地绘制各种图形和文本。SPI则是一种通信协议,它可以在STM32和其他器件之间进行快速的数据传输。使用STM32U8g2和SPI协议可以构建出各种有趣的应用,比如显示器、游戏控制器等。在进行开发过程中,需要注意硬件连接是否正确,以及软件编程是否符合规范。同时,还需要了解每个组件的功能和特点,以便更好地掌握整个系统的设计和实现。总之,STM32U8g2和SPI协议是可以十分灵活地组合使用的,能够满足不同应用场景的需求,是一个十分有用的开发组合。 ### 回答2: STM32是一款高性能的微控制器芯片,广泛应用于嵌入式系统和物联网设备中。U8g2是一个用于显示屏驱动的开源库,支持多种显示屏类型,并且具有灵活的配置选项。SPI是一种串行通信协议,用于在芯片之间传输数据。 在STM32中使用U8g2驱动SPI接口的显示屏可以实现高效的显示效果,并且具有较低的系统开销。通过使用SPI接口,芯片之间的数据传输速度可以达到高达10 Mbps,这使得在数据量较大的情况下也可以实现快速传输。使用STM32的硬件SPI接口进行数据传输可以减少系统负担,提高系统效率,对于需要快速响应和高并发的应用场景非常有利。 总之,STM32U8g2和SPI的高度集成和协作可以使嵌入式系统和物联网设备得到强大的输入输出能力和高效的运行能力。在实际应用中,使用这些技术可以提高系统的稳定性、安全性和效率。 ### 回答3: STM32是意法半导体公司推出的一款基于ARM Cortex-M内核的微控制器,具有高性能和低功耗等特点,广泛应用于工业控制、电子制造、家电等领域。而U8g2是一种开源的单色OLED显示库,常用于嵌入式设备中,支持多种控制模式(SPI、I2C等)和不同型号的OLED屏幕。 SPI是一种串行外设接口,常用于连接MCU和各种外设(如闪存、OLED屏幕等)。STM32支持硬件SPI接口,具有高速传输、低功耗、数据可靠性高等特点,在访问U8g2库时,可以利用SPI接口进行数据传输,提高数据传输效率。 当STM32U8g2库配合使用时,可以实现OLED屏幕的显示,以及通过SPI接口快速传输数据实现控制效果。在具体的应用中,可以根据需求选择不同的OLED屏幕和STM32型号,以及不同的SPI接口配置,达到更好的性能和应用效果。 总之,STM32U8g2库的结合可以帮助嵌入式开发者实现高效、可靠的OLED显示控制,具有广泛的应用前景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值