前言
这个系列的文章属于是为了一碟醋包了一顿饺子系列,起因是看到tb上某家店的ESP32C3开发板才9.9包邮。想着研究一下,把手头有个用Arduino UNO实现的项目升级一下,于是就有了这个系列。
ESP32C3的简介:
2020 年末,乐鑫推出安全、低功耗、低成本的 RISC-V MCU ESP32-C3。ESP32-C3 是一款安全稳定、低功耗、低成本的物联网芯片,搭载 RISC-V 32 位单核处理器,支持 2.4 GHz Wi-Fi 和 Bluetooth 5 (LE),为物联网产品提供行业领先的射频性能、完善的安全机制和丰富的内存资源。
环境信息
硬件
合宙ESP32C3开发板
中景园0.91寸128x32OLED显示屏SSD1306驱动(IIC版)
软件
ESP-IDF v4.4.4
驱动移植
1、获取&裁剪u8g2源码
1.1、u8g2的源码可以从github下载:https://github.com/olikraus/u8g2
1.2、解压&提取压缩文件中的csrc目录(u8g2支持c和c++,这里使用c的,复制csrc目录就行了)
1.3、把不需要的文件删除
除了自己器件外的其他"u8x8_d_器件名.c"的文件都删除,我的屏幕是0.91寸的oled(屏幕驱动SSD1306),所以除了"u8x8_d_ssd1306_128x32.c"其他都删除。可以创建个文件夹(u8g2)整理成如下结构(把.c跟.h分开):
u8g2
├─include
│ mui.h
│ mui_u8g2.h
│ u8g2.h
│ u8x8.h
│
└─src
mui.c
mui_u8g2.c
u8g2_bitmap.c
u8g2_box.c
u8g2_buffer.c
u8g2_button.c
u8g2_circle.c
u8g2_cleardisplay.c
u8g2_d_memory.c
u8g2_d_setup.c
u8g2_font.c
u8g2_fonts.c
u8g2_hvline.c
u8g2_input_value.c
u8g2_intersection.c
u8g2_kerning.c
u8g2_line.c
u8g2_ll_hvline.c
u8g2_message.c
u8g2_polygon.c
u8g2_selection_list.c
u8g2_setup.c
u8log.c
u8log_u8g2.c
u8log_u8x8.c
u8x8_8x8.c
u8x8_byte.c
u8x8_cad.c
u8x8_capture.c
u8x8_debounce.c
u8x8_display.c
u8x8_d_ssd1306_128x32.c
u8x8_fonts.c
u8x8_gpio.c
u8x8_input_value.c
u8x8_message.c
u8x8_selection_list.c
u8x8_setup.c
u8x8_string.c
u8x8_u16toa.c
u8x8_u8toa.c
1.4、把不需要的设备驱动初始化代码删除
删除 u8g2_d_setup.c中不必要的内容,只保留u8g2_Setup_ssd1306_i2c_128x32_univision_f
完成后u8g2_d_setup.c就剩下如下的内容(为了防止不同版本u8g2接口有改动,这边贴图只是示意要删成这样,别照抄图片中的代码):
这里类似的屏幕通信接口函数还有这些(这边只列举几个):
…
u8g2_Setup_ssd1306_128x32_univision_f
u8g2_Setup_ssd1306_i2c_128x32_univision_1
u8g2_Setup_ssd1306_i2c_128x32_univision_2
u8g2_Setup_ssd1306_i2c_128x32_univision_f
u8g2_Setup_ssd1306_i2c_128x32_winstar_f
…
其中,名字里面不带i2c的函数是SPI接口,函数最后的数字或字母,代表显示时的buf大小:
- 1 :128字节
- 2 :256字节
- f :1024字节
winstar跟univision可以理解成不同的驱动(我这款屏幕用univision正常,winstar会花屏)
1.5、把不需要的内存申请代码删除
删除 u8g2_d_memory.c中不必要的内容,只保留 u8g2_m_16_4_f函数(就是上一步函数体里面调用的)
由于用到的u8g2_Setup_ssd1306_i2c_128x32_univision_f函数中,只调用了u8g2_m_16_4_f这个函数,所以留下这个函数,其它的函数一定要删掉或注释掉,因为这里面的函数分配了大量的静态内存,会导致内存耗尽。
2、添加u8g2的component
2.1、在main目录的同级目录下,创建u8g2的component
idf create-component -C components/ u8g2
成功后会多出来一个文件夹:
2.2、按照1.3的目录结构把精简后的u8g2库文件放进来:
替换、覆盖同名文件。注意u8g2目录下有个u8g2.c,把它也移动到./src目录下面,整理好的目录结构如下:
PS C:\Espressif\frameworks\esp-idf-v4.4.4\examples\get-started\sample_project\components\u8g2> tree /F
C:.
│ CMakeLists.txt
│
├─include
│ mui.h
│ mui_u8g2.h
│ u8g2.h
│ u8x8.h
│
└─src
mui.c
mui_u8g2.c
u8g2.c
u8g2_bitmap.c
u8g2_box.c
u8g2_buffer.c
u8g2_button.c
u8g2_circle.c
u8g2_cleardisplay.c
u8g2_d_memory.c
u8g2_d_setup.c
u8g2_font.c
u8g2_fonts.c
u8g2_hvline.c
u8g2_input_value.c
u8g2_intersection.c
u8g2_kerning.c
u8g2_line.c
u8g2_ll_hvline.c
u8g2_message.c
u8g2_polygon.c
u8g2_selection_list.c
u8g2_setup.c
u8log.c
u8log_u8g2.c
u8log_u8x8.c
u8x8_8x8.c
u8x8_byte.c
u8x8_cad.c
u8x8_capture.c
u8x8_debounce.c
u8x8_display.c
u8x8_d_ssd1306_128x32.c
u8x8_fonts.c
u8x8_gpio.c
u8x8_input_value.c
u8x8_message.c
u8x8_selection_list.c
u8x8_setup.c
u8x8_string.c
u8x8_u16toa.c
u8x8_u8toa.c
2.3 添加文件到编译工具链
在u8g2目录下新建component.mk文件,写入以下内容:
COMPONENT_SRCDIRS:=.
COMPONENT_ADD_INCLUDEDIRS:=include
修改CmakeLists.txt,把里面替换为如下内容:
TODO:后续看看能不能改成通配符,这边是把所有的文件硬编码到CmakeLists
idf_component_register(SRCS "src/mui.c"
"src/mui_u8g2.c"
"src/u8g2_bitmap.c"
"src/u8g2_box.c"
"src/u8g2_buffer.c"
"src/u8g2_button.c"
"src/u8g2.c"
"src/u8g2_circle.c"
"src/u8g2_cleardisplay.c"
"src/u8g2_d_memory.c"
"src/u8g2_d_setup.c"
"src/u8g2_font.c"
"src/u8g2_fonts.c"
"src/u8g2_hvline.c"
"src/u8g2_input_value.c"
"src/u8g2_intersection.c"
"src/u8g2_kerning.c"
"src/u8g2_line.c"
"src/u8g2_ll_hvline.c"
"src/u8g2_message.c"
"src/u8g2_polygon.c"
"src/u8g2_selection_list.c"
"src/u8g2_setup.c"
"src/u8log.c"
"src/u8log_u8g2.c"
"src/u8log_u8x8.c"
"src/u8x8_8x8.c"
"src/u8x8_byte.c"
"src/u8x8_cad.c"
"src/u8x8_capture.c"
"src/u8x8_debounce.c"
"src/u8x8_display.c"
"src/u8x8_d_ssd1306_128x32.c"
"src/u8x8_fonts.c"
"src/u8x8_gpio.c"
"src/u8x8_input_value.c"
"src/u8x8_message.c"
"src/u8x8_selection_list.c"
"src/u8x8_setup.c"
"src/u8x8_string.c"
"src/u8x8_u16toa.c"
"src/u8x8_u8toa.c"
INCLUDE_DIRS "include")
做完这些,需要运行 idf.py reconfigure, 才可以在下次 idf.py build时生效component
3、实现u8g2依赖的功能接口
3.1、在u8g2.c中实现接口
u8g2通过switch的方式,访问需要的各种平台资源,这一点设计的很巧妙,也很灵活。
u8x8_gpio_and_delay提供平台的延时和gpio资源,u8x8_byte_i2c实现iic操作接口。
uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_tmsg, uint8_targ_int, void *arg_ptr)
uint8_t u8x8_byte_i2c(u8x8_t *u8x8, uint8_tmsg, uint8_targ_int, void *arg_ptr)
这里先给出一个实现好的示例:
/*
* @Author: xmprocat
* @Date: 2023-02-19 22:33:18
* @LastEditors: xmprocat
* @LastEditTime: 2023-02-20 23:04:36
* @Description:
*/
#include <stdio.h>
#include "esp_log.h"
#include "driver/i2c.h"
#include "u8g2.h"
#include "u8x8.h"
#define I2C_SCL_IO 5
#define I2C_SDA_IO 4
#define ACK_CHECK_EN 0x1 /*!< I2C master will check ack from slave*/
#define WRITE_BIT I2C_MASTER_WRITE /*!< I2C master write */
static const char *TAG = "u8g2";
i2c_config_t i2c_config = {
.mode = I2C_MODE_MASTER, // 主机模式
.sda_io_num = I2C_SDA_IO, // sda i引脚编号
.scl_io_num = I2C_SCL_IO, // scl 引脚编号
.sda_pullup_en = GPIO_PULLUP_ENABLE, // 上拉使能
.scl_pullup_en = GPIO_PULLUP_ENABLE, // 上拉使能
.master.clk_speed = 1000000 // 100k
};
static void _oled_i2c_init(void)
{
i2c_param_config(I2C_NUM_0, &i2c_config); // 配置参数初始化,此函数内部就是将i2c_config 中的相关参数 填入到i2c[i2c_num_0] 结构体中。
i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0); // 初始化配置以外的所有相关参数,将配置写入寄存器
}
void esp32_i2c_write(uint8_t addr, uint32_t idx, uint8_t *data)
{
i2c_cmd_handle_t handler = i2c_cmd_link_create();
i2c_master_start(handler);
i2c_master_write_byte(handler, addr | WRITE_BIT, ACK_CHECK_EN);
i2c_master_write(handler, data, idx, 2);
i2c_master_stop(handler);
i2c_master_cmd_begin(I2C_NUM_0, handler, 100 / portTICK_RATE_MS);
i2c_cmd_link_delete(handler);
}
// u8g2用到的系统资源
uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
switch (msg)
{
case U8X8_MSG_GPIO_AND_DELAY_INIT:
_oled_i2c_init(); //调用iic初始化
break;
case U8X8_MSG_DELAY_MILLI:
vTaskDelay(arg_int);
break;
default:
return 0;
}
return 1;
}
// u8g2用到的显示屏控制接口
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:
esp32_i2c_write(u8x8_GetI2CAddress(u8x8), buf_idx, buffer);
break;
default:
return 0;
}
return 1;
}
void u8g2Init(u8g2_t *u8g2)
{
u8g2_Setup_ssd1306_i2c_128x32_univision_f(u8g2, U8G2_R0, u8x8_byte_i2c, u8x8_gpio_and_delay); // 初始化 u8g2 结构体
u8g2_InitDisplay(u8g2); // 根据所选的芯片进行初始化工作,初始化完成后,显示器处于关闭状态
u8g2_SetPowerSave(u8g2, 0); // 打开显示器
u8g2_ClearBuffer(u8g2);
u8g2_SendBuffer(u8g2); // 清屏
ESP_LOGI(TAG, "u8g2 init done");
}
注意:我的这套方案采用的是硬件IIC,所以u8x8_gpio_and_delay函数中的
U8X8_MSG_DELAY_I2C
U8X8_MSG_GPIO_I2C_CLOCK
U8X8_MSG_GPIO_I2C_DATA
- 带有Delay字样的是延时相关东西,在case后面补充一些相应的延时函数,这些延时函数是用来模拟时序的,硬件IIC的话时序不需要软件控制,不管也行。
- 带有GPIO的是操控设备可能需要的一些引脚,如果用u8g2库里自带的软件模拟IIC或者软件模拟SPI函数去写设备的话,这些库自带的函数就会调用这里设置的电平函数,可以认为我们就是在这里告诉了u8g2库要怎么操作这个单片机的引脚或者延时。由于我们用的是硬件IIC,所以这里不用管也行。
- 带有MENU的是一些操控菜单的按键引脚啥的,如果用到菜单控制才去设置。
补充:由于ESP32与ESP32C3的硬件架构不同,以下通过用内联汇编的方式实现了毫秒级以下延时的代码在ESP32C3上无法正常使用(硬件IIC本身就不需要,但是有很多教程都写了这个就提一嘴)
static __inline void delay_clock(int ts)
{
uint32_t start, curr;
__asm__ __volatile__("rsr %0, ccount" : "=r"(start));
do
__asm__ __volatile__("rsr %0, ccount" : "=r"(curr));
while (curr - start <= ts);
}
#define delay_us(val) delay_clock(240*val)
#define delay_100ns(val) delay_clock(24*val)
4、效果展示
跨文件调用函数记得在.h里面声明
/*
* @Author: xmprocat
* @Date: 2023-02-19 20:09:31
* @LastEditors: xmprocat
* @LastEditTime: 2023-02-20 23:26:53
* @Description:
*/
#include "ssd1306_oled.h"
#include <stdio.h>
#include "esp_log.h"
#include "driver/i2c.h"
#include "u8g2.h"
#include "u8x8.h"
static const char *TAG = "ssd1306_oled";
void ssd1306_init(void)
{
u8g2_t u8g2;
u8g2Init(&u8g2);
u8g2_SetFont(&u8g2, u8g2_font_7x13_mr); // 设置英文字体
u8g2_DrawStr(&u8g2, 0, 13, "IP:255.255.255.255");
u8g2_DrawStr(&u8g2, 0, 32, "FreeRTOS#2023@Miao");
u8g2_SendBuffer(&u8g2); // 一定要发送buffer
}
需要完整工程的话私信吧
参考文献
https://zhuanlan.zhihu.com/p/587171646
https://blog.youkuaiyun.com/qq_43862401/article/details/121809470