u8g2 图形库(3):u8g2 移植到 STM32 平台

本文开发环境:

  • MCU型号:STM32F103ZET6
  • IDE环境: TrueStuido 9.3.0 / MDK 5.27
  • 代码生成工具:STM32CubeMx 5.4.0
  • HAL库版本:STM32Cube_FW_F1_V1.8.0

本文内容:

  1. STM32CubeMx 配置模板工程河 GPIO 口
  2. 移植 u8g2 到 MCU
  3. 附件:
    • MDK工程/TrueStuidio 工程(模拟 IIC)
    • u8g2 C程序源码

本系列文章索引:
u8g2 图形库(1):u8g2 图形库简介
u8g2 图形库(2):u8g2 入门指南
u8g2 图形库(3):u8g2 移植到 STM32 平台



一、准备

本文示例使用了 STM32CubeMx 配置外设的驱动,并生成一个 TrueStuido 工程。考虑到目前 MDK 的使用可能是更多的,所以附件包含了一个移植了 u8g2 的 MDK 工程模板。不过如果你使用MDK,移植过程会与本文示例所用的 TrueStuido 略有差异,在后文会提到。

前文可知,u8g2 支持非常多的控制器和多种总线协议,所以我们首先要确定所使用的总线和 OLED 显示屏,本文的OLED驱动IIC(控制器)为SSD1306,采用模拟IIC(软件IIC)的方式来控制:
在这里插入图片描述
至此,移植的目标已经确定:

  • 模拟IIC
  • SSD1306
  • STM32F103ZET6

接下来就是就具体的移植操作。

二、移植

1. 新建一个 MCU 工程

在移植一个库之前,新建一个工程是必须的,我们可以使用任何自己习惯的IDE来新建一个工程,这个工程作为基础,它需要单片机可以正常的控制IO口和延时。

1.1 配置基本外设

  • 选择目标单片机:STM32F103ZET6
    在这里插入图片描述
  • 开启 SW 调试接口:Debug 选项选择 Serial Wire
    在这里插入图片描述
  • 选择系统晶振源:High Speed Clock(HES) 选择 Crystal/Ceramic Rasonator,即使用外部高速晶振
    在这里插入图片描述
  • 配置系统各路时钟源:切换到Clock Configuration选项卡,在 HCLK(MHz)处输入 72,按回车按键让工具自动更新系统时钟树
    在这里插入图片描述
    上文根据所用单片机配置开启了一个新的工程,接着打开SW调试接口,选择晶振并配置了系统时钟,即完成了一个工程的基本配置,接着需要根据项目所需,来配合外设了。

1.2 配置特定外设资源

在本文中,我们用到软件IIC,所以只需要配置 2 个普通的IO 口为输出即可:
在这里插入图片描述
我们把PB11和PB10配置为输出,还可以进一步的定义它们的名字,具体在左侧 GPIO中,进入IO口的详细配置
在这里插入图片描述

1.3 生成工程

配置好以后,就可以生成工程了,STM32CubeMx 可以选择LL库还是HAL库,其中,LL和HAL库是可以并存的,也就是说,如果你工程配置了硬件IIC和硬件SPI,你可以指定IIC部分代码使用LL库,而SPI部分代码使用HAL库。
以下示例选择生成一个 TureStuido 工程:
在这里插入图片描述
本文还设置为每一个外设独立的生成. c/.h 文件,当然这一步是可选的,生成工程还有一些其他的配置可供用户选择:
在这里插入图片描述
最后点击右上角:GENERATE CODE 即可生成工程。

2. 移植

2.1 获取源码

首先需要获取源码,通常我们会在 github 上 下载源码工程,这可以保证程序是最新版本的:
u8g2 代码仓库https://github.com/olikraus/u8g2

可以选择下载压缩包,也可以使用 git clone 来对仓库进行克隆,在获取的文件夹内,命名为 csrc文件夹即是 u8g2 的源码程序。

2.2 添加源码

示例将 csrc 重命名为u8g2_csrc文件夹添加到了工程中:
在这里插入图片描述
为了是编译通过,需要添加头文件的路径(头文件统一在源码文件夹内),否则系统会找不到u8g2.h等头文件而无法编译:

  • 项目→属性→C/C++ General Paths and Symbols 中,点击右侧 Add... 添加头文件路径
    在这里插入图片描述

2.3 添加回调函数

在 u8g2 的构造函数中,需要应用程序传入一个函数指针,u8g2 库将通过这个函数指针来调用这个函数,这个函数用来现延时,IO口的控制等等。以下是这个函数的模板(它的函数名字是可以修改的):

uint8_t u8x8_gpio_and_delay_template(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
  switch(msg)
  {
    case U8X8_MSG_GPIO_AND_DELAY_INIT:	// called once during init phase of u8g2/u8x8
      break;							// can be used to setup pins
    case U8X8_MSG_DELAY_NANO:			// delay arg_int * 1 nano second
      break;    
    case U8X8_MSG_DELAY_100NANO:		// delay arg_int * 100 nano seconds
      break;
    case U8X8_MSG_DELAY_10MICRO:		// delay arg_int * 10 micro seconds
      break;
    case U8X8_MSG_DELAY_MILLI:			// delay arg_int * 1 milli second
      break;
    case U8X8_MSG_DELAY_I2C:				// arg_int is the I2C speed in 100KHz, e.g. 4 = 400 KHz
      break;							// arg_int=1: delay by 5us, arg_int = 4: delay by 1.25us
    case U8X8_MSG_GPIO_D0:				// D0 or SPI clock pin: Output level in arg_int
    //case U8X8_MSG_GPIO_SPI_CLOCK:
      break;
    case U8X8_MSG_GPIO_D1:				// D1 or SPI data pin: Output level in arg_int
    //case U8X8_MSG_GPIO_SPI_DATA:
      break;
    case U8X8_MSG_GPIO_D2:				// D2 pin: Output level in arg_int
      break;
    case U8X8_MSG_GPIO_D3:				// D3 pin: Output level in arg_int
      break;
    case U8X8_MSG_GPIO_D4:				// D4 pin: Output level in arg_int
      break;
    case U8X8_MSG_GPIO_D5:				// D5 pin: Output level in arg_int
      break;
    case U8X8_MSG_GPIO_D6:				// D6 pin: Output level in arg_int
      break;
    case U8X8_MSG_GPIO_D7:				// D7 pin: Output level in arg_int
      break;
    case U8X8_MSG_GPIO_E:				// E/WR pin: Output level in arg_int
      break;
    case U8X8_MSG_GPIO_CS:				// CS (chip select) pin: Output level in arg_int
      break;
    case U8X8_MSG_GPIO_DC:				// DC (data/cmd, A0, register select) pin: Output level in arg_int
      break;
    case U8X8_MSG_GPIO_RESET:			// Reset pin: Output level in arg_int
      break;
    case U8X8_MSG_GPIO_CS1:				// CS1 (chip select) pin: Output level in arg_int
      break;
    case U8X8_MSG_GPIO_CS2:				// CS2 (chip select) pin: Output level in arg_int
      break;
    case U8X8_MSG_GPIO_I2C_CLOCK:		// arg_int=0: Output low at I2C clock pin
      break;							// arg_int=1: Input dir with pullup high for I2C clock pin
    case U8X8_MSG_GPIO_I2C_DATA:			// arg_int=0: Output low at I2C data pin
      break;							// arg_int=1: Input dir with pullup high for I2C data pin
    case U8X8_MSG_GPIO_MENU_SELECT:
      u8x8_SetGPIOResult(u8x8, /* get menu select pin state */ 0);
      break;
    case U8X8_MSG_GPIO_MENU_NEXT:
      u8x8_SetGPIOResult(u8x8, /* get menu next pin state */ 0);
      break;
    case U8X8_MSG_GPIO_MENU_PREV:
      u8x8_SetGPIOResult(u8x8, /* get menu prev pin state */ 0);
      break;
    case U8X8_MSG_GPIO_MENU_HOME:
      u8x8_SetGPIOResult(u8x8, /* get menu home pin state */ 0);
      break;
    default:
      u8x8_SetGPIOResult(u8x8, 1);			// default return value
      break;
  }
  return 1;
}

上述程序可直接添加到 main 函数所在的c文件中,或者其他位置。到如果我们只需要特定的接口可以正常工作,那么这个函数的很多无关的 case语句就可以删除。u8g2 运行到和硬件有关系的程序时候,就需要调用这个回调函数,比如当u8g2库需要控制延时1ms的时候,它就会将msg设置为 U8X8_MSG_DELAY_MILLI,只要用户在这个 case 中正确的写入延时 1ms 的语句,u8g2 库一运行这个回调函数,就达到了延时1ms的效果,其余同理。

第一个 U8X8_MSG_GPIO_AND_DELAY_INIT,是延时函数和硬件IO口的初始化,这个STM32CubeMx 已经生成,所以不需要使用。

U8X8_MSG_DELAY_NANO,延时1ns这个事件,72M的单机无法做到,最简单的一个指令都已经超过这个时间。

其它的有如 SPI,8080,或片选等也是不需要的,最后简化后的函数如下:

void delay_us(uint32_t time)
{
  uint32_t i=8*time;
  while(i--);
}

uint8_t STM32_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
  switch(msg)
  {
  case U8X8_MSG_DELAY_100NANO:		// delay arg_int * 100 nano seconds
    __NOP();
    break;
  case U8X8_MSG_DELAY_10MICRO:		// delay arg_int * 10 micro seconds
	for (uint16_t n = 0; n < 320; n++)
    {
       __NOP();
    }
    break;
    case U8X8_MSG_DELAY_MILLI:			                // delay arg_int * 1 milli second
    	HAL_Delay(1);
        break;
    case U8X8_MSG_DELAY_I2C:				            // arg_int is the I2C speed in 100KHz, e.g. 4 = 400 KHz
    	delay_us(5);
    	break;							                // arg_int=1: delay by 5us, arg_int = 4: delay by 1.25us
    case U8X8_MSG_GPIO_I2C_CLOCK:		                // arg_int=0: Output low at I2C clock pin
        if(arg_int == 1)                                     // arg_int=1: Input dir with pullup high for I2C clock pin
        	HAL_GPIO_WritePin(GPIOB, SCL_Pin, GPIO_PIN_SET);
        else if(arg_int == 0)
        	HAL_GPIO_WritePin(GPIOB, SCL_Pin, GPIO_PIN_RESET);
    	break;
    case U8X8_MSG_GPIO_I2C_DATA:			            // arg_int=0: Output low at I2C data pin
        if(arg_int == 1)                                     // arg_int=1: Input dir with pullup high for I2C data pin
        	HAL_GPIO_WritePin(GPIOB, SDA_Pin, GPIO_PIN_SET);
        else if(arg_int == 0)
        	HAL_GPIO_WritePin(GPIOB, SDA_Pin, GPIO_PIN_RESET);
    	break;
    case U8X8_MSG_GPIO_MENU_SELECT:
        u8x8_SetGPIOResult(u8x8, /* get menu select pin state */ 0);
        break;
    case U8X8_MSG_GPIO_MENU_NEXT:
        u8x8_SetGPIOResult(u8x8, /* get menu next pin state */ 0);
        break;
    case U8X8_MSG_GPIO_MENU_PREV:
        u8x8_SetGPIOResult(u8x8, /* get menu prev pin state */ 0);
        break;
    case U8X8_MSG_GPIO_MENU_HOME:
        u8x8_SetGPIOResult(u8x8, /* get menu home pin state */ 0);
        break;
    default:
        u8x8_SetGPIOResult(u8x8, 1);			         // default return value
        break;
  }
  return 1;
}

注意到,上文程序中,还添加了delay_us()来实现us级别的延时。
U8X8_MSG_GPIO_I2C_CLOCK 中:

 case U8X8_MSG_GPIO_I2C_CLOCK:		                // arg_int=0: Output low at I2C clock pin
        if(arg_int == 1)                                     // arg_int=1: Input dir with pullup high for I2C clock pin
        	HAL_GPIO_WritePin(GPIOB, SCL_Pin, GPIO_PIN_SET);
        else if(arg_int == 0)
        	HAL_GPIO_WritePin(GPIOB, SCL_Pin, GPIO_PIN_RESET);
    	break;

注释表明,当arg_int参数为1时候,拉高时钟线IO口电平,反之,拉低。事实上笔者曾写:

 case U8X8_MSG_GPIO_I2C_CLOCK:		                // arg_int=0: Output low at I2C clock pin
        if(arg_int)                                     // arg_int=1: Input dir with pullup high for I2C clock pin
        	HAL_GPIO_WritePin(GPIOB, SCL_Pin, GPIO_PIN_SET);
        else if
        	HAL_GPIO_WritePin(GPIOB, SCL_Pin, GPIO_PIN_RESET);
    	break;

这种情况必须保证当 msgU8X8_MSG_GPIO_I2C_CLOCK 参数只有0和1,但是测试过程中发现似乎并不如此,所以本文不建议如此写法。其中 U8X8_MSG_GPIO_I2C_DATA也是同理。

另一个和模拟IIC 相关的事件U8X8_MSG_DELAY_MILLI,可以根据延时的时间来确定通信的频率,为了提高效率,本文直接延时1us。

设计好了回调函数,我们基本完成了移植,最后,我们需要构造函数来初始化u8g2,并调用显示函数来测试一下移植效果。

2.4 测试

2.4.1 构造函数

正如前文所提,u8g2 提供多种构造函数,他们的功能和绘图方式不尽相同,本文选用构造函数原型如下:

void u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2_t *u8g2, const u8g2_cb_t *rotation, u8x8_msg_cb byte_cb, u8x8_msg_cb gpio_and_delay_cb)

函数名u8g2_Setup_ssd1306_i2c_128x64_noname_f 表明一下几个信息:

  • OELD驱动IIC为SSD1306
  • OLED分辨率为128x64
  • 使用完整缓存的方式
  • 使用IIC通信
2.4.2 程序

main.c main 函数代码如下:

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */
  

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  u8g2_t u8g2;                                                                                     // a structure which will contain all the data for one display
  u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2, U8G2_R0, u8x8_byte_sw_i2c, STM32_gpio_and_delay);  // init u8g2 structure
  u8g2_InitDisplay(&u8g2);                                                                         // send init sequence to the display, display is in sleep mode after this,
  u8g2_SetPowerSave(&u8g2, 0);                                                                     // wake up display
  while (1)
  {
    /* USER CODE END WHILE */
	static int x = 30,y = 10;
	u8g2_ClearBuffer(&u8g2);
	u8g2_SetFont(&u8g2, u8g2_font_10x20_mf);
	u8g2_DrawStr(&u8g2, x,y,"u8g2");
	if(x >= 70)
	{
		x = y = 0;
	}
	else
	{
		x++;
		y++;
	}

	u8g2_SendBuffer(&u8g2);
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

第33行:是构造函数,它完成一些列的初始化函数,其中第1个参数指定了u8g2机构体,用以保存各种数据,第2个参数为选择的角度,可选择0,90,180,270,第3个则指定了通信的方式,第4个,需要将我们设计好的回调函数地址传给它,C语言中函数名就代表了函数的地址。

第34行:开始初始化OLED,它会根据所选的通信方式,对应具体的IO口,来进行一些列的初始化操作,这些初始化是库自带的,所以u8g2其实包含了各种显示屏驱动IC的驱动程序。

第35行:打开显示,默认是节能模式,显示屏是关闭的。

至此完成了移植和初始化,接着40行-42行调用库提供的函数绘图测。最后编译下载,可以发现一串“u8g2”的字符在左上角飘向右下角,并循环。

3. MDK 工程的移植

如果你使用 MDK 工程,那么上面一部分的操作是一致的,但是编译的时候会出现内存不足的情况:
在这里插入图片描述
这是因为在 u8g2_d-memory.c 中定义了非常多的全局变量,它们虽然不全被使用,但是 MDK 还是内存中给它们都分配了内存,导致内存空间不足,最直接的办法是在u8g2)d_setup.c中注释掉不被我们使用的构造函数定义:
在这里插入图片描述
这里把所有初始化函数,除了我们实际所用,都全部注释掉。
接着把 u8g2_d_memory.c 的也全部注释,并编译,根据报错提示,打开对应的变量即可:
在这里插入图片描述
最后编译下载程序,观察OLED显示是否正常。

三、附件

百度云链接:https://pan.baidu.com/s/1OsqcGLrGz7TMtkYwQp0Reg
提取码:ih25

你好!对于将 U8g2 移植STM32 上,你需要按照以下步骤进行操作: 1. 首先,确保你已经安装了适当的开发环境,包括 STM32CubeMX 和 STM32Cube HAL 。如果没有,请先下载和安装它们。 2. 打开 STM32CubeMX,并创建一个新的工程。选择适合你的 STM32 芯片型号,并配置所需的引脚和外设。 3. 在 "Middleware" 部分中,找到 "Graphics" 并选择 "U8g2"。 4. 根据你的需求选择所需的显示屏控制器和接口类型。U8g2 支持多种不同的控制器和接口。 5. 在 "Configuration" 选项中,配置 U8g2 的其他参数,比如显示屏分辨率、字体等。 6. 生成代码并打开工程。 7. 在生成的代码中,找到 "main.c" 文件,并添加以下代码来初始化 U8g2 : ```c #include "u8g2.h" u8g2_t u8g2; int main(void) { // 初始化代码 u8g2_Setup_<Controller>_<Interface>_u8g2(&u8g2, <rotation>, <u8x8_byte_fn>, <u8x8_gpio_and_delay_fn>); u8g2_InitDisplay(&u8g2); // 其他初始化代码 while (1) { // 显示内容更新代码 } } ``` 注意替换 `<Controller>` 和 `<Interface>` 为你选择的控制器和接口类型。`<rotation>` 是显示屏旋转角度,`<u8x8_byte_fn>` 和 `<u8x8_gpio_and_delay_fn>` 是用于与硬件通信的函数指针。 8. 在 `while (1)` 循环中,你可以使用 U8g2 提供的函数来更新显示屏上的内容。 这只是一个简单的示例,你可能还需要根据具体需求进行其他配置和修改。请参考 U8g2 的文档和示例代码来了解更多细节。希望对你有帮助!如果你有其他问题,请随时提问。
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值