草庐IT

U8g2图形库与STM32移植(I2C,软件与硬件)

frozencandles 2023-03-28 原文

U8g2图形库

简介

U8g2 是一个用于嵌入式设备的简易图形库,可以在多种 OLED 和 LCD 屏幕上,支持包括 SSD1306 等多种类型的底层驱动,并可以很方便地移植到 Arduino 、树莓派、NodeMCU 和 ARM 上。

U8g2 库同时包含了 U8x8 绘图库,两者的区别为:

  • U8g2 包含各种简单及复杂图形的绘制,并支持各种形式的字体,但需要占用一定单片机的内存作为绘图缓存
  • U8x8 只包含简单的显示文本功能,且只支持简单、定宽的字体。它直接绘制图形,没有缓存功能

U8g2 库的 GitHub 地址为:https://github.com/olikraus/u8g2 ,可以从中获取到源码与文档帮助。

移植

本次以将 U8g2 移植到 STM32 单片机与 SSD1306 通过 I2C 驱动的 128x64 OLED 为例,介绍移植的方法。不同单片机和驱动的移植可以参考这一过程,也可以参考 U8g2 的官方移植教程 https://github.com/olikraus/u8g2/wiki/Porting-to-new-MCU-platform

首先下载或克隆 U8g2 的源码,这里主要是使用 C 语言编写,所以只需要用到 csrc 目录下的文件。

下载完成后,将 csrc 目录拷贝或移动到工程目录里,并重命名为合适的目录名例如 u8g2lib

删除无用内容

接下来,需要删除一些无用的代码,并添加底层驱动的代码。

U8g2 的源码为了支持多种设备驱动,包含了许多兼容性的代码。首先,类似 u8x8_d_xxx.c 命名的文件中包含 U8x8 的驱动兼容,文件名包括驱动的型号和屏幕分辨率,因此需要删除无用的驱动文件,只保留当前设备的驱动。例如,本次使用的是 128x64 的 SSD1306 屏幕,那么只需要保留 u8x8_d_ssd1306_128x64_noname.c 文件,删除其它类似的文件即可。U8g2 支持的所有屏幕驱动可以在 https://github.com/olikraus/u8g2/wiki/u8g2setupc 找到。

同时还需要精简 u8g2_d_setup.cu8g2_d_memory.c 中 U8g2 提供的驱动兼容。

u8g2_d_setup.c 中,只需要保留 u8g2_Setup_ssd1306_i2c_128x64_noname_f() 这一个函数即可。注意,该文件内有几个命名类似的函数:命名中无 i2c 的是 SPI 接口驱动的函数,需要根据接口选择;以 1 结尾的函数代表使用的缓存空间为 128 字节,以 2 结尾的函数代表使用的缓存为 256字节,类似以 f 结尾的函数代表使用的缓存为 1024 字节。

u8g2_d_memory.c 文件也是同理,它需要根据 u8g2_d_setup.c 中的调用情况决定用到哪些函数。由于 u8g2_Setup_ssd1306_i2c_128x64_noname_f() 函数只用到 u8g2_m_16_8_f() 这一个函数,因此只需要保留它,其余函数全部删除即可。

还有一处必要的精简是字体文件 u8x8_fonts.cu8g2_fonts.c ,尤其是 u8g2_fonts.c ,该文件提供了包括汉字在内的几万个文字的多种字体,仅源文件就有 30MB ,编译后占据的内存非常大。

字体类型的变量非常多,建议先复制一个备份后将所有变量删除,之后视情况再添加字体。字体变量的命名大致遵循以下规则:

<prefix> '_' <name> '_' <purpose> <charset>

其中:

  • <prefix> 前缀基本上以 u8g2 开头;
  • <name> 字体名,其中可能包含字符大小
  • 各种 <purpose> 含义如下表所示:
名称 描述
t 透明字体形式
h 所有字符等高
m monospace 字体(等宽字体)
8 每一个字符都是 8x8 大小的
  • <charset> 是字体支持的字符集,如下表所示:
名称 描述
f 只包含单字节字符
r 只包含 ASCII 范围为 32~127 的字符
u 只包含 ASCII 范围为 32~95 的字符,即不包括小写英文
n 只包含数字及一些特殊用途字符
... 还包括许多自定义的字符集,例如有一些结尾带 gb2312 或 Chinese 的字体名就包括中文

一般建议只保留需要的字体即可。

添加回调函数

U8g2 已经包含了 SSD1306 的驱动,只需要添加一个函数 u8x8_gpio_and_delay() 用于模拟时序即可。官方文件给出了一个函数的编写模板为:

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:  // 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;
        /* and many other cases */
        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;
}

以下是一个写法示例:

uint8_t u8x8_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
            delay_ms(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
            arg_int ? GPIO_SetBits(GPIO_B, GPIO_Pin_6) : GPIO_ResetBits(GPIO_B, GPIO_Pin_6);  
            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
            arg_int ? GPIO_SetBits(GPIO_B, GPIO_Pin_7) : GPIO_ResetBits(GPIO_B, GPIO_Pin_7);  
            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;
}

如果使用的引脚不是 PB6 和 PB7 ,注意在对应的位置修改;如果是使用硬件 I2C 的方式,那么可以不需要模拟时序,但是需要编写硬件驱动函数。在结尾处,会给出一个基于标准库的硬件移植方法。

最后,不要忘记了初始化 I2C 对应的 GPIO 引脚。

U8g2简单使用

U8g2 的初始化可以参考如下步骤:

void u8g2_Init(u8g2_t *u8g2) {
    u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2, U8G2_R0, u8x8_byte_sw_i2c, u8x8_gpio_and_delay);  // 初始化 u8g2 结构体
    u8g2_InitDisplay(u8g2);      // 根据所选的芯片进行初始化工作,初始化完成后,显示器处于关闭状态
    u8g2_SetPowerSave(u8g2, 0);  // 打开显示器
    u8g2_ClearBuffer(u8g2);
}

这里需要调用之前保留的 u8g2_Setup_ssd1306_128x64_noname_f() 函数,该函数的4个参数,其含义为:

  • u8g2 :需要配置的 U8g2 结构体
  • rotation :配置屏幕是否要旋转,默认使用 U8G2_R0 即可
  • byte_cb :传输字节的方式,这里使用软件 I2C 驱动,因此使用 U8g2 源码提供的 u8x8_byte_sw_i2c() 函数。如果是硬件 I2C 的话,可以参照编写自己的函数
  • gpio_and_delay_cb :提供给软件模拟 I2C 的 GPIO 输出和延时,使用之前编写的配置函数 u8x8_gpio_and_delay()

如果需要显示字符串,需要提前调用以下函数设置字体:

void u8g2_SetFont(u8g2_t *u8g2, const uint8_t *font);

U8g2 的绘制方式有 2 种,每种都有不同的特点。

首先是全屏缓存模式(Full screen buffer mode),它的特点是绘制速度快,并且所有的绘制方法都可以使用。但是这种模式需要大量的 RAM 空间,因此使用需要用到缓存为 1024 字节的初始化函数(函数名以 f 结尾)。

这种绘图的方式首先需要清除缓冲区,调用绘图 API 后绘制的内容会保留在缓存内,需要手动发送缓存的内容到屏幕上:

u8g2_t u8g2;
u8g2_ClearBuffer(&u8g2);
/* Draw Something */
u8g2_SendBuffer(&u8g2);

第二种是分页模式(Page mode),它同样可以使用所有的绘制方法,但绘制速度较慢,不过占用的 RAM 空间也少,可以使用 128 或 256 字节的缓存(函数名以 1 和 2 结尾)。

这种绘图的方式首先创建第一页,然后在一个 do...while 循环内部绘制图形,不断判断是否到达下一页,如果到达了就自动刷新缓存:

u8g2_FirstPage(&u8g2);
do {
    /* Draw Something */
} while (u8g2_NextPage(&u8g2));

可以认为分页模式是一块一块绘制的。

还可以使用 U8x8 的绘图模式,这种情况下需要使用 U8x8 提供的结构体以及一系列函数,这里不再说明。

绘图API

完整的 API 参考可以参见官方文档 https://github.com/olikraus/u8g2/wiki/u8g2reference/ ,里面不仅有 API 的介绍,还有绘制效果的图片演示。

U8g2 的坐标系和绝大多数 GUI 库一样,原点在左上角,(x, y) 往右下递增,坐标的单位为像素。

简单图形绘制

void u8g2_DrawPixel(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y);
void u8g2_DrawHLine(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t len);
void u8g2_DrawVLine(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t len);
void u8g2_DrawLine(u8g2_t *u8g2, u8g2_uint_t x1, u8g2_uint_t y1, u8g2_uint_t x2, u8g2_uint_t y2);

分别用于绘制像素点、根据左上角顶点 (x, y) 与长度 len 绘制水平线与垂直线,以及绘制两点之间的线段。

void u8g2_DrawFrame(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t w, u8g2_uint_t h);
void u8g2_DrawBox(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t w, u8g2_uint_t h);

根据左上角的 (x, y) 坐标与宽 wh 绘制空心与实心矩形。

void u8g2_DrawRBox(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t w, u8g2_uint_t h, u8g2_uint_t r);
void u8g2_DrawRFrame(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t w, u8g2_uint_t h, u8g2_uint_t r);

绘制实行与空心圆角矩形,多了一个参数圆角半径 r

void u8g2_DrawCircle(u8g2_t *u8g2, u8g2_uint_t x0, u8g2_uint_t y0, u8g2_uint_t rad, uint8_t option);
void u8g2_DrawDisc(u8g2_t *u8g2, u8g2_uint_t x0, u8g2_uint_t y0, u8g2_uint_t rad, uint8_t option);

根据圆心 (x0, y0) 绘制直径为 rad ×2+1 的空心圆和实心圆。

option 为圆的部分选项,此参数可控制绘制圆弧:

取值 结果
U8G_DRAW_ALL 整个圆弧
U8G2_DRAW_UPPER_RIGHT 右上部分的圆弧
U8G2_DRAW_UPPER_LEFT 左上部分的圆弧
U8G2_DRAW_LOWER_LEFT 左下部分的圆弧
U8G2_DRAW_LOWER_RIGHT 右下部分的圆弧

还可以使用按位或运算符 | 连接几个部分。

void u8g2_DrawEllipse(u8g2_t *u8g2, u8g2_uint_t x0, u8g2_uint_t y0, u8g2_uint_t rx, u8g2_uint_t ry, uint8_t option);
void u8g2_DrawFilledEllipse(u8g2_t *u8g2, u8g2_uint_t x0, u8g2_uint_t y0, u8g2_uint_t rx, u8g2_uint_t ry, uint8_t option);

根据圆心 (x0, y0) 和水平半径 rx 、竖直半径 ry 绘制空心和实心椭圆。

void u8g2_DrawTriangle(u8g2_t *u8g2, int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2);

根据三个点绘制实心三角形(空心三角形可以使用直线达到类似效果)。

void u8g2_DrawXBM(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t w, u8g2_uint_t h, const uint8_t *bitmap);

在图形左上角 (x, y) 根据宽 wh 绘制 XBM 格式的位图。可以使用 https://tools.clz.me/image-to-bitmap-array 工具将一般图片转换为位图代码。

和 Bitmap 有关的函数还有一个:

void u8g2_SetBitmapMode(u8g2_t *u8g2, uint8_t is_transparent);

该函数用于设置 Bitmap 是否透明。

字符显示

为了显示字符串,首先要设置字体。调用以下函数可以提前设置字体:

void u8g2_SetFont(u8g2_t *u8g2, const uint8_t *font);
void u8g2_SetFontMode(u8g2_t *u8g2, uint8_t is_transparent);

字体是一种特殊的位图,因此也可以设置是否透明。所有的字体保存在 u8g2_fonts.c 源文件中,注意在移植 U8g2 库时曾经裁剪过该文件。

u8g2_uint_t u8g2_DrawStr(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str);

在左下角 (x, y) 处显示字符串。注意,这个方法只能绘制 ASCII 字符。如有需要显示 Unicode 字符,需要使用以下函数:

u8g2_uint_t u8g2_DrawGlyph(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, uint16_t encoding);
u8g2_uint_t u8g2_DrawUTF8(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str);

绘制 Unicode 字符和字符串。U8g2 支持 16 位的 Unicode 字符集,因此 encoding 的范围被限制在 65535 。该函数绘制 Unicode 字符串时还需要对应的字体也支持 Unicode 字符。

注意这几个函数都有返回值,它们返回绘制成功的字符个数。

#define u8g2_GetAscent(u8g2)
#define u8g2_GetDescent(u8g2)

这两个宏定义用于获取字体基线以上和基线以下的高度。上文提到的显示字符串的函数实际上参数 y 指的是基线高度。此外注意基线以下的高度返回的是负值。

u8g2_uint_t u8g2_GetStrWidth(u8g2_t *u8g2, const char *s);
u8g2_uint_t u8g2_GetUTF8Width(u8g2_t *u8g2, const char *str);

获取当前字体下,字符串和 UTF-8 字符串的宽度,单位为像素。

void u8g2_SetFontDirection(u8g2_t *u8g2, uint8_t dir);

设置文字朝向,根据参数不同分别设置为正常朝向的顺时针旋转 dir ×90° 。

其它绘图相关API

void u8g2_SetClipWindow(u8g2_t *u8g2, u8g2_uint_t clip_x0, u8g2_uint_t clip_y0, u8g2_uint_t clip_x1, u8g2_uint_t clip_y1);

设置采集窗口大小,设置后绘制的图形只在该窗口范围内显示。设置后可以使用 u8g2_SetMaxClipWindow() 函数去掉该限制。

示例代码

以下官方示例代码可以在 OLED 上显示该库的 logo :

u8g2_t u8g2;
u8g2_FirstPage(&u8g2);
do {
    u8g2_SetFontMode(&u8g2, 1);
    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_inb30_mn);
    u8g2_DrawStr(&u8g2, 21, 8, "8");
    u8g2_SetFontDirection(&u8g2, 0);
    u8g2_SetFont(&u8g2, u8g2_font_inb24_mf);
    u8g2_DrawStr(&u8g2, 51, 30, "g");
    u8g2_DrawStr(&u8g2, 67, 30, "\xb2");
    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_SetFont(&u8g2, u8g2_font_4x6_tr);
    u8g2_DrawStr(&u8g2, 1, 54, "github.com/olikraus/u8g2");
} while (u8g2_NextPage(&u8g2));

首发于:http://frozencandles.fun/archives/301

附录:使用硬件I2C移植U8g2

硬件 I2C 效率上比软件 I2C 快了非常多,因此特别适合 U8g2 这种大型 UI 框架。下面基于标准库介绍硬件 I2C 的移植方式。

如果使用硬件 I2C ,需要在调用该函数(或类似函数)时,使用自己的硬件读写函数:

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);

首先还是需要编写一个 gpio_and_delay() 回调函数。不过由于这里是使用硬件 I2C ,因此不再需要提供 GPIO 和时序操作的支持,只需要提供一个毫秒级的延时即可:

uint8_t u8x8_gpio_and_delay_hw(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
            break;
        case U8X8_MSG_DELAY_10MICRO: // delay arg_int * 10 micro seconds
            break;
        case U8X8_MSG_DELAY_MILLI: // delay arg_int * 1 milli second
            Delay_ms(1);
            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_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;
}

如果是使用硬件 I2C ,那么需要自行编写硬件驱动函数,向 OLED 写入字节。这个函数的编写可以参考官方提供的软件驱动函数 u8x8_byte_sw_i2c() ,一个编写示例为:

uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
    uint8_t* data = (uint8_t*) arg_ptr;
    switch(msg) {
        case U8X8_MSG_BYTE_SEND:
            while( arg_int-- > 0 ) {
                I2C_SendData(I2C1, *data++);
                while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) 
                    continue;
            }
            break;
        case U8X8_MSG_BYTE_INIT:
        /* add your custom code to init i2c subsystem */
            RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
            I2C_InitTypeDef I2C_InitStructure = {
                .I2C_Mode = I2C_Mode_I2C,
                .I2C_DutyCycle = I2C_DutyCycle_2,
                .I2C_OwnAddress1 = 0x10,
                .I2C_Ack = I2C_Ack_Enable,
                .I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit,
                .I2C_ClockSpeed = 400000
            };
            I2C_Init(I2C1, &I2C_InitStructure);
            I2C_Cmd(I2C1, ENABLE);  
            break;
        case U8X8_MSG_BYTE_SET_DC:
        /* ignored for i2c */
            break;
        case U8X8_MSG_BYTE_START_TRANSFER:
            while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
            I2C_GenerateSTART(I2C1, ENABLE);
            while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
                continue;
            I2C_Send7bitAddress(I2C1, 0x78, I2C_Direction_Transmitter);
            while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
                continue;
            break;
        case U8X8_MSG_BYTE_END_TRANSFER:
            I2C_GenerateSTOP(I2C1, ENABLE);
            break;
        default:
            return 0;
    }
    return 1;
}

从各个 case 标签可以很明白地看出一个 I2C 的读写过程:U8X8_MSG_BYTE_INIT 标签下需要初始化 I2C 外设,U8X8_MSG_BYTE_START_TRANSFER 标签产生起始信号并发出目标地址,U8X8_MSG_BYTE_SEND 标签开始发送字节,并且发送的字节存储在 *arg_ptr 参数中,arg_int 是字节的总长度( U8g2 库似乎一次不会传输多余 32 字节的信息)。最后,U8X8_MSG_BYTE_END_TRANSFER 标签处产生停止信号。

注意在使用硬件 I2C 时,GPIO 需要设置为复用开漏输出模式 GPIO_Mode_AF_OD

最后一步,用以上编写的硬件函数初始化 U8g2 驱动:

u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2, U8G2_R0, u8x8_byte_hw_i2c, u8x8_gpio_and_delay_hw);

硬件移植过程完毕。

有关U8g2图形库与STM32移植(I2C,软件与硬件)的更多相关文章

  1. STM32读取串口传感器数据(颗粒物传感器,主动上传) - 2

    文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,

  2. ruby-on-rails - 可移植 Ruby on Rails 环境 - 2

    我给自己买了一个新的8gigUSBkey,我正在寻找一个合适的解决方案来拥有一个可移植RoR环境来学习。我在谷歌上搜索了一下,发现了一些可能性,但我很想听听一些现实生活中的经历和意见。谢谢! 最佳答案 我喜欢InstantRails,非常容易使用,无需安装程序,也不会修改您的系统环境。 关于ruby-on-rails-可移植RubyonRails环境,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/q

  3. STM32的HAL和LL库区别和性能对比 - 2

    LL库和HAL库简介LL:Low-Layer,底层库HAL:HardwareAbstractionLayer,硬件抽象层库LL库和hal库对比,很精简,这实际上是一个精简的库。LL库的配置选择如下:在STM32CUBEMX中,点击菜单的“ProjectManager”–>“AdvancedSettings”,在下面的界面中选择“AdvancedSettings”,然后在每个模块后面选择使用的库总结:1、如果使用的MCU是小容量的,那么STM32CubeLL将是最佳选择;2、如果结合可移植性和优化,使用STM32CubeHAL并使用特定的优化实现替换一些调用,可保持最大的可移植性。另外HAL和L

  4. ESP32学习入门:WiFi连接网络 - 2

    目录一、ESP32简单介绍二、ESP32Wi-Fi模块介绍三、ESP32Wi-Fi编程模型四、ESP32Wi-Fi事件处理流程 五、ESP32Wi-Fi开发环境六、ESP32Wi-Fi具体代码七、ESP32Wi-Fi代码解读6.1主程序app_main7.2自定义代码wifi_init_sta()八、ESP32Wi-Fi连接验证8.1测试方法8.2服务器模拟工具sscom58.3测试代码8.4测试结果前言为了开发一款亚马逊物联网产品,开始入手ESP32模块。为了能够记录自己的学习过程,特记录如下操作过程。一、ESP32简单介绍ESP32是一套Wi-Fi(2.4GHz)和蓝牙(4.2)双模解决方

  5. Spring Security 6.0系列【32】授权服务器篇之默认过滤器 - 2

    有道无术,术尚可求,有术无道,止于术。本系列SpringBoot版本3.0.4本系列SpringSecurity版本6.0.2本系列SpringAuthorizationServer版本1.0.2源码地址:https://gitee.com/pearl-organization/study-spring-security-demo文章目录前言1.OAuth2AuthorizationServerMetadataEndpointFilter2.OAuth2AuthorizationEndpointFilter3.OidcProviderConfigurationEndpointFilter4.N

  6. ruby - 摘要::CRC32 与 Zlib - 2

    在我的代码中,我需要使用各种算法(包括CRC32)对文件进行哈希处理。因为我还在Digest系列中使用其他加密哈希函数,所以我认为为它们维护一个一致的接口(interface)会很好。为了记录,我确实找到了digest-crc,一颗完全符合我要求的gem。问题是,Zlib是标准库的一部分,并且有一个我想重用的CRC32工作实现。此外,它是用C编写的,因此它应该提供与digest-crc相关的卓越性能,后者是纯ruby​​实现。实现Digest::CRC32一开始看起来非常简单:%w(digestzlib).each{|f|requiref}classDigest::CRC32一切正常:

  7. ruby - 安装gem : Couldn't reserve space for cygwin's heap, Win32错误487错误 - 2

    我正在尝试在我的机器上安装win32-apigem,但在构建native扩展时我遇到了一些问题:$geminstallwin32-api--no-ri--rdocTemporarilyenhancingPATHtoincludeDevKit...Buildingnativeextensions.Thiscouldtakeawhile...C:\Programs\dev_kit\bin\make.exe:***Couldn'treservespaceforcygwin'sheap,Win32error0ERROR:Errorinstallingwin32-api:ERROR:Failed

  8. Ruby 1.9 - 没有这样的文件可以加载 'win32/open3' - 2

    我在Windows上运行ruby​​1.9.2并试图移植在Ruby1.8中工作的代码。该代码使用以前运行良好的Open4.popen4。对于1.9.2,我做了以下事情:通过geminstallPOpen4安装了POpen4需要POpen4通过require'popen4'尝试像这样使用POpen4:Open4.popen4("cmd"){|io_in,io_out,io_er|...}当我这样做时,我得到了错误:nosuchfiletoload--win32/open3如果我尝试安装win32-open3(geminstallwin32-open3),我会收到错误消息:win32-op

  9. Dell Inspiron 5488加内存32G - 2

    DellInspiron5488加内存32G 原装内置内存仅仅8G,目前看,真的太小了! 1.内存型号Dell5488内存型号:DDR42666。笔记本有两个内存插槽,原装占了一个,还能扩展一个。 2.买内存如果买Dell原装笔记本内存,8G就得500块左右。 我咨询了一下,三星的笔记本内存,可以兼容。16G,299块(2023年2月23日,京东价) Dell5488内存组合,最多只能插两根16G内存。 我于是买了两根三星16G内存。装上,很爽😄 跑国产系统统信UOS,再也看不到用交换区了,32G内存,爽!  

  10. ruby - 如何在 Ruby 中构建可移植的绝对路径? - 2

    让我们假设一个脚本需要访问一个目录,比如在“任意”操作系统上的/some/where/abc。在Ruby中构建路径有几个选项:File.join('','some','where','abc')File.absolute_path("some#{File::SEPARATOR}where#{File::SEPARATOR}abc",File::SEPARATOR)标准API中的路径名我相信第一个解决方案足够清晰,但符合惯用语。根据我的经验,一些代码审查会要求评论来解释它的作用......问题有没有更好的方法来构建绝对路径是Ruby,其中更好的意思是“完成工作并为自己说话”?

随机推荐