STM32驱动0.96寸OLED显示图片

  1. 将需要显示的图片(以jpeg格式为例)通过 convertio 转换为 bmp 格式;
  2. 打开 Windows 画图编辑图片,将图片缩放为 128x64 分辨率大小;
  3. 打开 搞定设计在线PS(photopea 中国版),点击菜单栏 图像->调整->阈值 可以在二值化过程中顺便去除噪点,保存成 bmp 格式;
  4. 打开取模工具 PCDtoLCD2002,导入第三步保存的 bmp 图片后预览效果。打开字模选项,按下图红圈方式设置后点击确定按钮保存设置,最后点击生成生成一个长度为 1024 的一维数组,复制备用;

  1. 粘贴下面代码到源文件中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const unsigned char images[1][1024] = {
{...你的图片...},
};

// 显示图片(通过改变索引值和图片对应的像素可以显示不同像素的图片)
// x,y: 图片的起点坐标
// px,py: 图片的像素(与实际取模的图片像素一致)
// index: 图片索引
void OLED_ShowBMP(u8 x, u8 y, u8 px, u8 py, u8 index, u8 mode)
{
u8 temp,t1;
u16 j,i;
u8 y0=y;

i = (px/2)*(py/4);

for(j = 0; j < i;j++)
{
temp = images[index][j]; //调用图片
for(t1=0;t1<8;t1++)
{
if(temp&0x80)OLED_DrawPoint(x,y,mode);
else OLED_DrawPoint(x,y,!mode);
temp<<= 1;
y++;
if((y-y0) == py)
{
y=y0;
x++;
break;
}
}
}
}
  1. 调用方法显示图片
1
2
OLED_ShowBMP(0, 0, 128, 64, 0, 1);
OLED_Refresh_Gram(); //更新显示到OLED
NOTES

如果你需要显示多张图片,可以把其它图片加到 images 数组中,然后在调用 OLED_ShowBMP 函数时注意指定 index 为相应的值即可。

参考资料

  1. 0.96寸OLED显示图片
  2. 怎样在Photoshop中二值化

怎么跑起来一个docker项目

用户反馈他们的应用不能正常发布,原因是线上编译器报错。我拿到用户的应用源文件后,本地测试可以正常编译部署。于是联系 Cloud 团队是否能提供 docker 镜像方便我复现线上环境,看看是不是环境问题。Cloud 团队负责人直接丢给我一个项目连接,打开后只有代码源文件和一个 Dockerfile 配置文件。

怎么从一个 Dockerfile 文件启动一个 docker 容器呢?

首先,我们构建 (build) 这个项目成一个镜像 (container image):

1
2
cd <project-folder> # 切换到项目根目录
docker build -t <image-name> . # 构成镜像

接着,我们就可以运行这个镜像了:

1
docker run <image-name>

修复ClashX HTTP代理端口和Socks5端口总是为0的问题

因为,ClashX 默认监听的是 7890 端口,怀疑是端口被占用了,我们使用 lsof 命令来查看端口被占用情况。

1
2
3
> lsof -i :7890
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
privoxy 579 lolimay 3u IPv4 0x527fa2f554e1edb 0t0 TCP localhost:7890 (LISTEN)

可以看到,端口被 privoxy 这个应用给占用了。接着我们用 ps 命令查看占用这个端口的进程。

1
2
3
> ps -ef | grep privoxy
UID PID PPID C STIME TTY TIME CMD
501 94421 1 0 10:43AM ?? 0:00.01 /Users/lolimay/Library/Application Support/ShadowsocksX-NG/privoxy --no-daemon privoxy.config

可以看到是 ShadowsocksX-NG 这个软件占用的,我们打开这个应用把它的监听端口改成 8790 或其他端口后,重启 ClashX 发现 ClashX 已经可以正常监听 7890 端口了 🎉

STM32F1的IO口简介

基本介绍

STM32的引脚可设置为可设置为:普通IO功能、复用功能、重映射功能。不过普通IO功能、复用功能用得比较多。

  1. 普通 IO 功能一般需要使能 GPIOAGPIOBGPIOC 时钟
  2. 需要使用复用功能,如 STM32F103RCT6 的串口1功能,需要使用 PA9PA10 两个引脚,此时需要使用串口1时钟 USART1
  3. 需要用到外设的重映射功能时需要使能 AFIO 的时钟。

关于重映射功能举例如下:

1
2
3
4
重映射USART2
USART2的TX/RX在PA.2/3
PA.2已经被Timer2的channel3使用
需要把USART2的TX/RX重映射到PD.5/6

库函数的调用步骤如下:

  1. 使能被重新映射到的I/O端口时钟

    1
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);

  2. 使能被重新映射的外设时钟

    1
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);

  3. 使能AFIO功能的时钟(勿忘!)

    1
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

  4. 进行重映射

    1
    GPIO_PinRemapConfig(GPIO_Remap_USART2, ENABLE);

初始化步骤

  1. 使用 RCC_APB2PeriphClockCmd 函数使能相关 APB2 外设时钟
  2. 通过 GPIO_Init 函数初始化并配置各IO口
  3. 使用 GPIO_ReadInputDataBit 读取IO口电平状态,使用 GPIO_SetBitsGPIO_ResetBits 来设置各IO口的值 (实际我们使用 PAout(0)=1, PBout(2)=0; 这种写法去设置 IO 口的电平状态)

相关函数定义和说明如下:

1. GPIO_Init 初始化函数: 配置IO口的模式和速度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);

typedef enum {
GPIOA,
GPIOB,
GPIOC
} GPIO_TypeDef;

typedef struct {
unit16_t GPIO_Pin;
GPIOSpeed_TypeDef GPIO_Speed;
GPIOMode_TypeDef GPIO_Mode;
} GPIO_InitTypeDef;

typedef enum {
GPIO_Mode_AIN = 0x0, // 模拟输入
GPIO_Mode_IN_FLOATING = 0x04, // 浮空输入
GPIO_Mode_IPD = 0x28, // 下拉输入
GPIO_Mode_IPU = 0x48, // 上拉输入
GPIO_Mode_Out_OD = 0x14, // 开漏输出
GPIO_Mode_Out_PP = 0x10, // 通用推挽输出
GPIO_Mode_AF_OD = 0x1C, // 复用开漏输出
GPIO_Mode_AF_PP = 0x18 // 复用推挽输出
} GPIOMode_TypeDef;

typedef enum {
GPIO_Speed_10MHz = 1,
GPIO_Speed_2MHz,
GPIO_Speed_50MHz
} GPIOSpeed_TypeDef;

示例:

1
2
3
4
5
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; // PB5 端口设置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 速度 50MHz
GPIO_Init(GPIOB, &GPIO_InitSture); // 根据参数配置IO口 PB5

2. GIPO_ReadInputDataBit: 读取IO口的电平状态

1
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, unit16_t GPIO_Pin);

示例:

1
2
// 返回值是 1(Bit_SET) 或者 0(Bit_RESET)
GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1); // 读取IO口 PA1 的电平状态

3. GPIO_Write: 一次性设置GPIO的多个端口值(通过ODR寄存器实现)

1
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);

4. 通过 GPIO_SetBits 和 GPIO_ResetBits 来操作各IO口的值

1
2
void GPIO_SetBits(GPIO_TypeDef* GPIOx, unit16_t GPIO_Pin);
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, unit16_t GPIO_Pin);

示例:

1
2
GPIO_SetBits(GPIOC, GPIO_Pin_3); // 设置IO口 PC3 输出 1
GPIO_ReSetBits(GPIOC, GPIO_Pin_3); // 设置IO口 PC3 输出 0

5. 通过 RCC_APB2PeriphClockCmd 使能IO时钟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* RCC_APB2Periph_AFIO 功能复用IO时钟
* 需要用到外设的重映射功能时才需要使能AFIO时钟
*
* RCC_APB2Periph_GPIOA GPIOA时钟
* RCC_APB2Periph_GPIOB GPIOB时钟
* RCC_APB2Periph_GPIOC GPIOC时钟
* RCC_APB2Periph_USART1 USART1时钟(串口1时钟)PA9(TX)、PA10(RX)
*/
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
typedef enum {
Enable = 1,
Disable = 0
} FunctionalState;

参考链接

  1. STM32端口复用和重映射
  2. 什么情况下需要使能AFIO的时钟呢?

SSD1306 8080并口读写过程

  1. 先根据要写入/读取的数据类型,设置 RS 为1(数据)/0(命令)
1
OLED_RS = cmd; // cmd = 0(命令)/1(数据)
  1. 然后拉低片选 CS
1
OLED_CS = 0;
  1. 接着我们根据要读数据还是要写数据置RD/WR为低,然后
    • 读数据:在RD上升沿,读取数据线上的数据
    • 写数据:在WR上升沿,使数据写入到驱动IC里面
1
2
3
4
5
6
// 读数据
OLED_RD = 0;
OLED_RD = 1;
// 写数据
OLED_WR = 0;
OLED_WR = 1;

参考资料

  1. SSD1306 官方手册
  2. Proteus仿真库中英文对照

Keil uVision5+Proteus STM32 开发仿真调试一条龙服务

调试技巧

Source Code Debugging (源码联调)

首先 Keil 端,输出二进制文件格式选择 elf 格式,而不是 hex 或者 axf (axf 其实就是 elf 格式,但是后缀名不一样),因为 elf 格式的二进制文件是带有调试信息的,包含C代码和编译后的汇编代码的映射。这样我们可以直接在更具有可读性的 C 代码上打断点和调试,而不是直接怼汇编代码。

生成好 elf 文件后,我们打开 Proteus 双击 CMU 导入 elf 文件。点击 Start VSM Debugging 可以直接在 C 代码中打断点单步调试。在进入调试模式后,点击菜单栏 Debug 按钮,弹出的列表最下面几项会显示原理图中主要元件的调试选项,如 CM3 -> Source Code - U1 可以进入源代码调试、CM3 -> Registers - U1 可以查看 Cortex-M3 处理器的各个寄存器的状态、SSD1306 LCD Controller RAM - LCD 可以查看 LCD 控制器 SSD1306 内存状态。

Design Explorer

画好原理图后,如果不确定连线是不是都连接上了,可以打开 Design Explorer 视图查看元件各个端口的网络标号 (Net),以此判断是否已连接上。

Probes (探针)

其次,我们可以使用 Proteus 提供的 Probes (探针) 元件 (在左侧工具栏中可以找到),给待测试的线路标上电压探针会电流探针来方便我们判断电路状态。

Terminal Voltages & Terminal Logic States

除了使用探针,在 Proteus 进入单步调试后,我们可以点击各个元件,此时会显示该元件的各个端口电压 (Terminal Voltages) 和各个端口逻辑状态 (Terminal Logic States),也可以方便我们调试。

其中,Proteus 逻辑状态表如下:

逻辑状态 说明 数字逻辑
FLT 悬空态,高阻态 -
WUD 未定义态,与模拟电压混联 -
CON 竞争态,与数字电压冲突 -
SHI 主动输出高逻辑 1
SLO 强电低态,主动输出低逻辑 0
PHI 电源高态,电源高逻辑 1
PLO 电源低态,电源低逻辑 0
WHI 弱电高态,被动输出高逻辑 1
WLO 弱电低态,被动输出低逻辑 0

(非常好用) Diagnostics Configuration

Proteus 还提供一个杀手级的功能,Configure Diagnostics 可以在 Debug 菜单列表中找到或选中元件后右键菜单中找到。通过这个功能,我们可以配置各个元件在日志中输出各种事件,比如我们需要查看 UG-2864HSWEG01 (LCD, SSD1306 Controller included) 关于 Contoller DiagnosticsTrace Information,我们可以把这个 Diagnostics 的 Trace Information Level 改成 Debug 级别。这样我们就可以根据这些事件有没有发生,来判断我们的代码有没有生效。

如通过下面这条日志,我们可以知道 SSD1306 此时使用的是 8080 并行接口的连接方式:

Animation Circuits Configuration

进入 System -> Set Animation Options,Proteus 默认只启用了显示引脚的逻辑状态动画,进入该项设置页后,我们可以把:

  • Show Voltage & Current on Probes 在探针上显示电压和电流
  • Show Logic State of Pins 在引脚上显示逻辑状态 (默认开启)
  • Show Wire Voltage by Color 用颜色标注导线的电压
  • Show Wire Current With Arrows 在导线上标注电流的方向

这些设置项全部开启,这些动画信息可以有效地帮助我们进行调试。

问题排查思路

如果使用了上诉调试技巧还是找不到问题所在,但是日志中显示 proteus simulation is not running in real time due to excessive cpu load 警告,提示你 由于 CPU 超负荷了 Proteus 的仿真不能实时进行。这个警告⚠️很有可能是导致仿真失败、无法复现在真实硬件上的效果的真正原因,建议解决方案如下:

  • 调低 CMU 的晶振频率,如 STM32F103RCT6 的实际晶振频率为 72M,你可以根据你电脑性能的实际情况调整至 比如 1M。双击 CMU 后在 Crystal Frequency 输入框中填写 1M 即可
  • Proteus 进入 System -> Set Simulation Options 调低仿真精度,但是作用其实有限
  • 打开 任务管理器,选中 Proteus 进程右键 转置详细信息,选中进程 PDS.EXE 右键设置设置优先级 实时。这样可以让该进程可以分到更多的 CPU 计算资源,但是作用有限
  • 换一台高性能的设备 /狗头

STM32F103各型号对比

STM32F 系列属于中低端的32位ARM微控制器,该系列芯片是意法半导体(ST)公司出品,其内核是Cortex-M3,STM32F103 则是其中一个子系列。

STM32F103 系列 MCU (微控制器,Microcontroller Unit) 统一采用 ARM Cortex-M3 内核,CPU 最高速度 72 MHz。该系列产品的闪存 Flash 大小由 16KB ~ 1MB 不等,支持多种控制外设、USB全速接口以及 CAN 总线接口

STM32F103 各型号含义如下

STM32F103 引脚数 Flash闪存容量 封装工艺 温度
T: 36 Pin 4: 16KB H: BGA 6: Industrial Temperature range -40℃ to -85℃
C: 48 Pin 6: 32KB T: LQFP 7: Industrial Temperature range -40℃ to -105℃
R: 64 Pin 8: 64KB H: WLCSP64
V: 100 Pin B: 128KB
Z: 144 Pin C: 256KB
D: 384KB
E: 512KB
F: 768KB
G: 1MB

例如,某 MiniSTM32F103开发板V3版本,使用 STM32F103RCT6 作为 MCU。那么我们可以判断出这款 MCU 的参数如下:48 Pin,256KB FLASH,使用 LQFP 工艺封装,工作温度在 -40℃ ~ -85℃ 之间。

更完整分类,请参考官方选型手册:

参考资料

  1. 关于 STM32 软硬件兼容性相关的知识

OpenGauss解锁Locked的账号

在开发 OpenGauss 数据库驱动时,使用同一账户重复登录的次数过多,会导致账户被锁定。

这时候,我们可以使用 gsql 命令行工具,登录另一个管理员账户给这个账号解封:

1
alter user lolimay account unlock;

参考资料

  1. openGauss1.0.0 用户被锁

把十进制表示的字节数组转换为十六进制表示

1
2
3
4
5
6
7
8
9
const arr = '150 82 45 39 181 90 66 61 68 125 221 223 184 235 226 20 52 152 174 14 132 180 221 206 15 10 219 141 96 224 148 78'

const hexBytesToIntArray = bytes => {
return bytes
.map(s => parseInt(s)) // 以 10 进制解析字符串为数字
.map(n => n.toString(16).padStart(4, '0x0')) // 将转为 16 进制表示填充前置0
}

hexBytesToIntArray(arr.split(' '))

需要注意的是,不能把 parseInt 直接作为参数传进 map 中,因为 map 的回调函数签名为 (data: any, index?: number) => any,而 parseInt 的函数签名为 (str: string, radix?: number) => number,如果直接把 parseInt 则默认会把 index 作为 radix (进制)传给 parseInt 导致解析错误。

另,上面提到 parseInt 可以接受一个可选进制 radix 参数,所以把上述方法反过来,也可以完成十六进制表示的数据转换为10进制数据,如:

1
parseInt('0xff', 16).toString(10) // 255

清空当前工作区

之前切换到其它分支时,但是碰到当前工作区有脏修改的情况一直是用的

1
git stash

来把工作区的修改保存在暂存区。其实这种做法是不太优雅的,因为我的目的是丢弃当前工作区的修改。今天特地 Google 了一下,发现了丢弃当前工作区修改的正确方式是:

1
git checkout -- .