本文开发环境:
- MCU型号:STM32F103ZET6
- IDE环境: TrueStuido 9.3.0 / MDK 5.27
- 代码生成工具:STM32CubeMx 5.4.0
- HAL库版本:STM32Cube_FW_F1_V1.8.0
本文内容:
- STM32CubeMx 配置模板工程河 GPIO 口
- 移植 u8g2 到 MCU
- 附件:
- 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;
这种情况必须保证当 msg
为 U8X8_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