U8g2 是一个用于嵌入式设备的简易图形库,可以在多种 OLED 和 LCD 屏幕上,支持包括 SSD1306 等多种类型的底层驱动,并可以很方便地移植到 Arduino 、树莓派、NodeMCU 和 ARM 上。
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.c 和 u8g2_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.c 和 u8g2_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 的初始化可以参考如下步骤:
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 参考可以参见官方文档 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) 坐标与宽 w 高 h 绘制空心与实心矩形。
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) 根据宽 w 高 h 绘制 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° 。
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 效率上比软件 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);
硬件移植过程完毕。
文章目录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.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,
我给自己买了一个新的8gigUSBkey,我正在寻找一个合适的解决方案来拥有一个可移植RoR环境来学习。我在谷歌上搜索了一下,发现了一些可能性,但我很想听听一些现实生活中的经历和意见。谢谢! 最佳答案 我喜欢InstantRails,非常容易使用,无需安装程序,也不会修改您的系统环境。 关于ruby-on-rails-可移植RubyonRails环境,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/q
LL库和HAL库简介LL:Low-Layer,底层库HAL:HardwareAbstractionLayer,硬件抽象层库LL库和hal库对比,很精简,这实际上是一个精简的库。LL库的配置选择如下:在STM32CUBEMX中,点击菜单的“ProjectManager”–>“AdvancedSettings”,在下面的界面中选择“AdvancedSettings”,然后在每个模块后面选择使用的库总结:1、如果使用的MCU是小容量的,那么STM32CubeLL将是最佳选择;2、如果结合可移植性和优化,使用STM32CubeHAL并使用特定的优化实现替换一些调用,可保持最大的可移植性。另外HAL和L
目录一、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)双模解决方
有道无术,术尚可求,有术无道,止于术。本系列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
在我的代码中,我需要使用各种算法(包括CRC32)对文件进行哈希处理。因为我还在Digest系列中使用其他加密哈希函数,所以我认为为它们维护一个一致的接口(interface)会很好。为了记录,我确实找到了digest-crc,一颗完全符合我要求的gem。问题是,Zlib是标准库的一部分,并且有一个我想重用的CRC32工作实现。此外,它是用C编写的,因此它应该提供与digest-crc相关的卓越性能,后者是纯ruby实现。实现Digest::CRC32一开始看起来非常简单:%w(digestzlib).each{|f|requiref}classDigest::CRC32一切正常:
我正在尝试在我的机器上安装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
我在Windows上运行ruby1.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
DellInspiron5488加内存32G 原装内置内存仅仅8G,目前看,真的太小了! 1.内存型号Dell5488内存型号:DDR42666。笔记本有两个内存插槽,原装占了一个,还能扩展一个。 2.买内存如果买Dell原装笔记本内存,8G就得500块左右。 我咨询了一下,三星的笔记本内存,可以兼容。16G,299块(2023年2月23日,京东价) Dell5488内存组合,最多只能插两根16G内存。 我于是买了两根三星16G内存。装上,很爽😄 跑国产系统统信UOS,再也看不到用交换区了,32G内存,爽!
让我们假设一个脚本需要访问一个目录,比如在“任意”操作系统上的/some/where/abc。在Ruby中构建路径有几个选项:File.join('','some','where','abc')File.absolute_path("some#{File::SEPARATOR}where#{File::SEPARATOR}abc",File::SEPARATOR)标准API中的路径名我相信第一个解决方案足够清晰,但符合惯用语。根据我的经验,一些代码审查会要求评论来解释它的作用......问题有没有更好的方法来构建绝对路径是Ruby,其中更好的意思是“完成工作并为自己说话”?