
这是第一篇关于stm32的笔记。第一篇反而不是基础知识的记录,这一点真是五味杂陈。
u8g2是一个oled库,在arduino上常用做oled显示效果。这里我将讲述u8g2移植到stm32平台的一些步骤,本博客将尽量详细(为了我以后方便)。
1、u8g2的下载:连接在这里 https://github.com/olikraus/u8g2,下载解压之后,我们只需要 “csrc”文件夹就可以了。里面有我们移植需要的.c文件和.h文件。

只要这个文件夹里的文件
2、将.c文件导入工程,将.h文件加入编译路径。(这个我就不贴图了)
3、然后在你的工程里引入下图的这两个头文件

引用这两个头文件
4、根据oled的主控型号、分辨率、品牌(有时候可能没有),选择必要的.c文件,并删除其他的oled支持。
我以的手上的.96英寸分辨率128x64的oled(主控ssd1306)为例:
刚刚在keil里引入了很多的.c文件,绝大部分都是各种oled的支持程序,找到与你屏幕对应的文件,我的是下面这个:

我的显示屏对应的驱动
然后把其他屏幕的驱动在keil去除引用:

这样删除
一般“u8x8_d_xxxxxxx”这种就是屏幕驱动,都可以删,但是下面几个切记不要删除

不要删
5、去“u8g2_d_setup.c”中,选择支持你屏幕的通讯协议的函数,然后把其他的删掉。

找到支持你屏幕通讯协议的函数
也可以在u8g2.h里找,这样会快一点,然后去“u8g2_d_setup.c”,用“ctrl+f”搜索定位即可。
因为我是spi协议,所以我选择图里这个:

我选择的是"u8g2_Setup_ssd1306_128x64_noname_f"这个函数,其他都删除了。当然也可以使用后缀为1或者2的函数,比较省空间,但是自然的会比较耗时。具体差异如下:

这里我选择的是u8g2_Setup_ssd1306_128x64_noname_f,即表示一帧的的大小为1024(128*8)bytes.
然后在"u8g2_d_setup.c"里找到这个函数,复制出来,然后把其他函数全删了。(当然你注释也行)最后"u8g2_d_setup.c"里之后剩下这样:

6、然后需要写个回调函数。写法可以参考官方教程 https://github.com/olikraus/u8g2/wiki/Porting-to-new-MCU-platform也可以看我下面怎么做: 这里先放一个官方的模板
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;
}
*INIT是引脚和Delay的初始化,这里要加上oled和delay的初始化,以及延时函数。如果用的是HAL库,这些都自动完成了,这里直接break就好。
*带有Delay字样的是延时相关东西,在case后面补充一些相应的延时函数,这些延时函数是用来模拟时序的,硬件IIC的话时序不需要软件控制,不管也行。
*带有GPIO的是操控设备可能需要的一些引脚,根据自己实际的引脚去选择地写一些就好了。我们要写的内容就是根据arg_int这个传入参数去调用设置电平的函数,是1则为高,是0则为低。
*带有MENU的是一些操控菜单的按键引脚啥的,如果用到菜单控制才去设置。
后面如果用u8g2库里自带的软件模拟IIC或者软件模拟SPI函数去写设备的话,这些库自带的函数就会调用这里设置的电平函数,可以认为我们就是在这里告诉了u8g2库要怎么操作这个单片机的引脚或者延时。这些都是库自动返回变量“msg”实现的。因为我们是用软件模拟SPI,所以我们要在这个模板的基础上,定义各个"msg"时的管脚状态。
按照这个思路以及我手头的oled与stm32引脚的连接,我的回调函数如下:
uint8_t u8x8_stm32_gpio_and_delay(U8X8_UNUSED u8x8_t *u8x8,U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int,U8X8_UNUSED void *arg_ptr)
{
switch (msg)
{
case U8X8_MSG_GPIO_SPI_DATA:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_7):GPIO_ResetBits(GPIOA,GPIO_Pin_7);
break;
case U8X8_MSG_GPIO_SPI_CLOCK:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_5):GPIO_ResetBits(GPIOA,GPIO_Pin_5);
break;
case U8X8_MSG_GPIO_AND_DELAY_INIT:
OLED_Init();
delay_ms(10);
break;
case U8X8_MSG_DELAY_MILLI:
delay_ms(arg_int);
break;
case U8X8_MSG_GPIO_CS:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_4):GPIO_ResetBits(GPIOA,GPIO_Pin_4);
case U8X8_MSG_GPIO_DC:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_6):GPIO_ResetBits(GPIOA,GPIO_Pin_6);
break;
case U8X8_MSG_GPIO_RESET:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_3):GPIO_ResetBits(GPIOA,GPIO_Pin_3);
break;
}
return 1;
} 其中的oled_Init和delay_ms是我自己的函数实现,实际上需要你自己完成这两个功能。然后放在main函数下,声明即可。
7、再回到"u8g2_Setup_ssd1306_128x64_noname_f"函数

红框中的就是"u8g2_Setup_ssd1306_128x64_noname_f"函数可以使用的通信协议,如果你想用模拟IIC或者模拟SPI的话,其实官方是有写好给你直接用的,官方是这么说的:

就是说,如果想用软件模拟时序的话,官方已经把模拟时序的相关函数写好了,这些函数里面相关的引脚电平设置函数和延时函数用的就是我们在第八步里面告诉它的。可以直接用上面表格第一列的几个函数作为你的回调函数,这样就不用再写了。但是在这里我有必要介绍下,硬件SPI或者I2C时怎么做通信协议的回调函数。
回调函数是这种形式的:typedef uint8_t (*u8x8_msg_cb)(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)。一样的,函数名字可以随意,但输入参数没错就好了。官方给出了硬件SPI和硬件IIC的模板,分别如下:
extern "C" uint8_t u8x8_byte_arduino_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
uint8_t *data;
uint8_t internal_spi_mode;
switch(msg) {
case U8X8_MSG_BYTE_SEND:
data = (uint8_t *)arg_ptr;
while( arg_int > 0 ) {
SPI.transfer((uint8_t)*data);
data++;
arg_int--;
}
break;
case U8X8_MSG_BYTE_INIT:
u8x8_gpio_SetCS(u8x8, u8x8->display_info->chip_disable_level);
SPI.begin();
break;
case U8X8_MSG_BYTE_SET_DC:
u8x8_gpio_SetDC(u8x8, arg_int);
break;
case U8X8_MSG_BYTE_START_TRANSFER:
/* SPI mode has to be mapped to the mode of the current controller, at least Uno, Due, 101 have different SPI_MODEx values */
internal_spi_mode = 0;
switch(u8x8->display_info->spi_mode) {
case 0: internal_spi_mode = SPI_MODE0; break;
case 1: internal_spi_mode = SPI_MODE1; break;
case 2: internal_spi_mode = SPI_MODE2; break;
case 3: internal_spi_mode = SPI_MODE3; break;
}
SPI.beginTransaction(SPISettings(u8x8->display_info->sck_clock_hz, MSBFIRST, internal_spi_mode));
u8x8_gpio_SetCS(u8x8, u8x8->display_info->chip_enable_level);
u8x8->gpio_and_delay_cb(u8x8, U8X8_MSG_DELAY_NANO, u8x8->display_info->post_chip_enable_wait_ns, NULL);
break;
case U8X8_MSG_BYTE_END_TRANSFER:
u8x8->gpio_and_delay_cb(u8x8, U8X8_MSG_DELAY_NANO, u8x8->display_info->pre_chip_disable_wait_ns, NULL);
u8x8_gpio_SetCS(u8x8, u8x8->display_info->chip_disable_level);
SPI.endTransaction();
break;
default:
return 0;
}
return 1;
}
uint8_t u8x8_byte_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
static uint8_t buffer[32]; /* u8g2/u8x8 will never send more than 32 bytes between START_TRANSFER and END_TRANSFER */
static uint8_t buf_idx;
uint8_t *data;
switch(msg)
{
case U8X8_MSG_BYTE_SEND:
data = (uint8_t *)arg_ptr;
while( arg_int > 0 )
{
buffer[buf_idx++] = *data;
data++;
arg_int--;
}
break;
case U8X8_MSG_BYTE_INIT:
/* add your custom code to init i2c subsystem */
break;
case U8X8_MSG_BYTE_SET_DC:
/* ignored for i2c */
break;
case U8X8_MSG_BYTE_START_TRANSFER:
buf_idx = 0;
break;
case U8X8_MSG_BYTE_END_TRANSFER:
i2c_transfer(u8x8_GetI2CAddress(u8x8) >> 1, buf_idx, buffer);
break;
default:
return 0;
}
return 1;
} 以硬件IIC为例,接下来就照猫画虎,跟着硬件IIC的模板稍微改一下就好了。主要是两个地方:
一个是IIC初始化,如果使用HAL库会自动生成了初始化代码并且在主函数调用了,这里我们不填也行
另外一个是那个i2c_transfer()函数要换成HAL库的。HAL库的IIC写函数有两个,一个是HAL_I2C_Master_Transmit(),另外一个是HAL_I2C_Mem_Write(),后者一般用于器件中还有内存或者寄存器地址的情况,比如EEPROM等。所以我们用前者。
还有一个需要注意的事情,大坑,就是u8x8_GetI2CAddress(u8x8)这里返回来的是已经右移了的器件地址,0.96寸oled一般是0x78,然后HAL库里面的也是要我们填右移了的器件地址,所以不要像模板那样再往左移动一位了(可能别的啥库函数需要左移后的原始地址吧,为啥不统一呢,太坑了!!!)
over,综上,我们的代码如下:
uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
static uint8_t buffer[32]; /* u8g2/u8x8 will never send more than 32 bytes between START_TRANSFER and END_TRANSFER */
static uint8_t buf_idx;
uint8_t *data;
switch(msg){
case U8X8_MSG_BYTE_SEND:
data = (uint8_t *)arg_ptr;
while( arg_int > 0 ){
buffer[buf_idx++] = *data;
data++;
arg_int--;
}
break;
case U8X8_MSG_BYTE_INIT:
/* add your custom code to init i2c subsystem */
break;
case U8X8_MSG_BYTE_START_TRANSFER:
buf_idx = 0;
break;
case U8X8_MSG_BYTE_END_TRANSFER:
HAL_I2C_Master_Transmit(&hi2c1,u8x8_GetI2CAddress(u8x8), buffer, buf_idx,1000);
break;
default:
return 0;
}
return 1;
} 8、初始化
调用第5步剩下的"u8g2_Setup_ssd1306_128x64_noname_f"这个函数来初始化,第一个参数是一个空的结构体地址,第二个参数表示是否旋转,第四个参数是我们写的gpio的那个回调函数名字,第三个参数是我们写的另外一个回调函数名或者我截图表格里那几个模拟时序的函数名。第四个参数是第6步建立的回调函数
#include "u8x8.h"
#include "u8g2.h"
u8g2_t u8g2;
u8g2_Setup_ssd1306_128x64_noname_f(&u8g2,U8G2_R0,u8x8_byte_4wire_sw_spi,u8x8_stm32_gpio_and_delay);
u8g2_InitDisplay(&u8g2);
u8g2_SetPowerSave(&u8g2, 0); 9、到这里就可以尝试编译了,如果出现很多错误,特别是内存不足!!!不用怀疑。这是正常现象,下面将着手解决。
(1)这是因为在 中定义了非常多的全局变量,它们虽然不全被使用,但是 MDK 还是内存中给它们都分配了内存,导致空间不足。我们把 的全部注释,并编译

根据报错提示,打开对应的变量即可:

10、u8g2测试:
代码如下:
#include "stm32f10x.h"
#include "delay.h"
#include "oled.h"
#include "u8x8.h"
#include "u8g2.h"
int t=0;
u8g2_t u8g2;
uint8_t u8x8_stm32_gpio_and_delay(U8X8_UNUSED u8x8_t *u8x8,U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int,U8X8_UNUSED void *arg_ptr);
int main(void){
SysTick_Init(72);
u8g2_Setup_ssd1306_128x64_noname_f(&u8g2,U8G2_R0,u8x8_byte_4wire_sw_spi,u8x8_stm32_gpio_and_delay);
u8g2_InitDisplay(&u8g2);
u8g2_SetPowerSave(&u8g2, 0);
while(1){
u8g2_ClearBuffer(&u8g2);
if(++t >= 32) t = 1;
u8g2_DrawCircle(&u8g2,64,32,t,U8G2_DRAW_ALL);
u8g2_DrawCircle(&u8g2,32,32,t,U8G2_DRAW_ALL);
u8g2_DrawCircle(&u8g2,96,32,t,U8G2_DRAW_ALL);
u8g2_SendBuffer(&u8g2);
delay_ms(100);
}
}
uint8_t u8x8_stm32_gpio_and_delay(U8X8_UNUSED u8x8_t *u8x8,U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int,U8X8_UNUSED void *arg_ptr)
{
switch (msg)
{
case U8X8_MSG_GPIO_SPI_DATA:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_7):GPIO_ResetBits(GPIOA,GPIO_Pin_7);
break;
case U8X8_MSG_GPIO_SPI_CLOCK:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_5):GPIO_ResetBits(GPIOA,GPIO_Pin_5);
break;
case U8X8_MSG_GPIO_AND_DELAY_INIT:
OLED_Init();
delay_ms(10);
break;
case U8X8_MSG_DELAY_MILLI:
delay_ms(arg_int);
break;
case U8X8_MSG_GPIO_CS:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_4):GPIO_ResetBits(GPIOA,GPIO_Pin_4);
case U8X8_MSG_GPIO_DC:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_6):GPIO_ResetBits(GPIOA,GPIO_Pin_6);
break;
case U8X8_MSG_GPIO_RESET:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_3):GPIO_ResetBits(GPIOA,GPIO_Pin_3);
break;
}
return 1;
} 效果如下:

11、到了这一步好像皆大欢喜,但实际上还有一个大问题。不能输出字符,启用字符功能,你将会出现上百的错误:
为了说明我把代码放上来:
#include "stm32f10x.h"
#include "delay.h"
#include "oled.h"
#include "u8x8.h"
#include "u8g2.h"
int t=0;
u8g2_t u8g2;
uint8_t u8x8_stm32_gpio_and_delay(U8X8_UNUSED u8x8_t *u8x8,U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int,U8X8_UNUSED void *arg_ptr);
void draw(u8g2_t *u8g2);
int main(void){
SysTick_Init(72);//���һ��Ҫ��
u8g2_Setup_ssd1306_128x64_noname_f(&u8g2,U8G2_R0,u8x8_byte_4wire_sw_spi,u8x8_stm32_gpio_and_delay);
u8g2_InitDisplay(&u8g2);
u8g2_SetPowerSave(&u8g2, 0);
while(1){
u8g2_FirstPage(&u8g2);
do
{
draw(&u8g2);
} while (u8g2_NextPage(&u8g2));
}
}
uint8_t u8x8_stm32_gpio_and_delay(U8X8_UNUSED u8x8_t *u8x8,U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int,U8X8_UNUSED void *arg_ptr)
{
switch (msg)
{
case U8X8_MSG_GPIO_SPI_DATA:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_7):GPIO_ResetBits(GPIOA,GPIO_Pin_7);
break;
case U8X8_MSG_GPIO_SPI_CLOCK:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_5):GPIO_ResetBits(GPIOA,GPIO_Pin_5);
break;
case U8X8_MSG_GPIO_AND_DELAY_INIT:
OLED_Init();
delay_ms(10);
break;
case U8X8_MSG_DELAY_MILLI:
delay_ms(arg_int);
break;
case U8X8_MSG_GPIO_CS:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_4):GPIO_ResetBits(GPIOA,GPIO_Pin_4);
case U8X8_MSG_GPIO_DC:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_6):GPIO_ResetBits(GPIOA,GPIO_Pin_6);
break;
case U8X8_MSG_GPIO_RESET:
(arg_int)?GPIO_SetBits(GPIOA,GPIO_Pin_3):GPIO_ResetBits(GPIOA,GPIO_Pin_3);
break;
}
return 1;
}
void draw(u8g2_t *u8g2)
{
u8g2_SetFontMode(u8g2, 1); // Transparent
u8g2_SetFontDirection(u8g2, 0);
u8g2_SetFont(u8g2, u8g2_font_inb24_mf);
u8g2_DrawStr(u8g2, 0, 20, "U");
u8g2_SetFontDirection(u8g2, 1);
u8g2_SetFont(u8g2, u8g2_font_inb24_mf);
u8g2_DrawStr(u8g2, 21,8,"8");
u8g2_SetFontDirection(u8g2, 0);
u8g2_SetFont(u8g2, u8g2_font_wqy12_t_chinese1);
u8g2_DrawStr(u8g2, 51,30,"g");
u8g2_DrawUTF8(u8g2,10,50,"你好,world");
u8g2_DrawHLine(u8g2, 2, 35, 47);
u8g2_DrawHLine(u8g2, 3, 36, 47);
u8g2_DrawVLine(u8g2, 45, 32, 12);
u8g2_DrawVLine(u8g2, 46, 33, 12);
}
错误信息:

还是老问题,内存不足。这是因为在"u8g2_fonts.c"里定义了很多字库,它们虽然不全被使用,但是 MDK 还是内存中给它们都分配了内存,导致空间不足。我们可以选择想要的字库,不想要的删除即可。
程序里我只用了两个字库:

所以我把其他字库删除,编译成功,效果如下:

12、怎么输出中文?
(1)使用中文字库
(2)使用DrawUTF8函数

(3)编译器编码设置为UTF-8
