LoRa网关项目——OLED(SSD1306)开发(一)
#前言
最近在做一个LoRa物联网网关的项目,网关的作用主要是管理连接的LoRa传感器终端,将传感数据通过协议转换向上转发到Internet,当然,也要处理下行的数据。
使用到的LoRa射频芯片是SX1278,MCU为STM32F103RCT6,连接Internet用的是ESP8266+AT,且移植了FreeRTOS(单纯是为了学习),开发环境是STM32CubeMX+Keil 5。由于之前没负责过整个系统的开发,所以开此贴记录一下开发过程,由于本人上学以来语文一直不好,所以文笔正在努力进步中,如果此文章有您觉得我说的不明白的地方,可以发送邮件到wanglu082@yeah.net,或者在文章下方评论,我看到会尽快回复您,多谢谅解!
为了方便查看网关的状态,本系统加入了一个0.96‘OLED模块来显示一些调试信息。
一. 对SSD1306驱动改进
1.1 初始化函数改进
拿SSD1306的初始化来说,OLED模块厂商给的驱动是使用MCU发送多次命令对SSD1306进行配置,这就要启动多次IIC通讯,但实际上,通过DataSheet中给出的实例通信时序,SSD1306是支持一次发送多次命令/数据的:
所以我们可以将初始化的命令序列组成一个数组(1Byte 命令、1Byte数据、1Byte 命令、1Byte数据……),通过一次IIC通信发送给SSD1306。改进后 OLED_Init()
函数如下:
void OLED_Init(void) {
/* GPIO的初始化在MX_GPIO_Init()中进行 */
/* 初始化命令数组必须定义为 全局变量 或 局部静态变量,
若定义为局部变量,则可能 OLED_Init 执行结束,DMA没有传输完成 */
static u8 OledInitCmd[29] =
{0xAE,0x00,0x10,0x40,0xB0,0x81,0xFF,0xA1,0xA6,0xA8,
0x3F,0xC8,0xD3,0x00,0xD5,0x80,0xD8,0x05,0xD9,0xF1,
0xDA,0x12,0xDB,0x30,0x8D,0x14,0xAF,0x20,0x00
};
u16 OledInitCmdLength = sizeof(OledInitCmd);
HAL_I2C_Mem_Write_DMA(&hi2c1, OLED_IIC_ADDR, OLED_CMD, I2C_MEMADD_SIZE_8BIT, OledInitCmd, OledInitCmdLength);
}
这里为什么用 HAL_I2C_Mem_Write_DMA()
而不是 HAL_I2C_Master_Transmit_DMA()
呢?
单看函数的描述很难分别其区别,通过对比这两个函数的输入参数的不同,HAL_I2C_Mem_Write_DMA()
多了两个形参:MemAddress 、MemAddSize 。其实看看源码也就明白了,HAL_I2C_Mem_Write_DMA()
会在发送完Salve Address后在发送8/16 bit的 MemAddress ,然后才是n byte的 数据。
那么为什么SSD1306要发送 MemAddress 呢?再看通讯时序图,在控制位的前面有2bit的Co和D/C#,先说D/C#用于标志该字节的数据是命令(Command)还是数据(Data),对应到SSD1306就是该字节是一条控制命令还是对GDDRAM的修改,Co是非常重要的1个控制位,Co为0表示接下来的每个字节都是数据,这个数据不是与命令相区别的数据,而是代表以下的每个字节都不含Co和D/C# 位,默认与前面相同。
正因为有这种机制,才需要调用HAL_I2C_Mem_Write_DMA()
,使在发送SlaveAddr后能发送一个字节的控制命令(0x00或0x40)
-
0x00 ;Co 和 D/C# 均为0,表示接下来都是纯命令,不需要再携带Co 和 D/C#。
-
0x40;Co 为0, D/C# 为1,表示接下来都是纯数据,对GDDRAM的修改,也不需要再携带Co 和 D/C#。
1.2 显示函数重写
拿显示一个point来说,SSD1306的驱动中使用如下的流程:
- 定位需要操作的page
- 对该page的特定位进行操作
这两步操作是两次IIC通讯过程,看似没什么问题,但是我们的显示器通常不是一直显示一个页面的。如配合按键实现的多级菜单例程中,我最初是使用先对局部写0,再写入数据的方法来实现。这样也能做,就是延迟有点大。
在生活中,其实显示器都会以一定的频率在不断的刷新,显示显存中的内容。如果还是使用官方实例中的方式,那么在While循环中实现刷新就必须要保存当前显示的信息,而且整个IIC通讯过程还必须要CPU参与,极大的降低了CPU的利用率,这时候就可以让DMA出场了。
DMA是数据的搬运工,它独立于CPU工作,只需要在开始传送和传送结束时通知CPU,CPU就有工夫去干大事。那怎么才能让DMA参与进来呢?毕竟它只能干搬数据的活,通过DataSheet中以下的文字描述可以了解到,SSD1306可以用过IIC通信向其内部GDDRAM进行操作(内部RAM结构讲解:STM32使用OLED模块(SSD1306):OLED_DrawBMP()),只需要将IIC的 D/C 位置 1。
那我们是不是就能一次性将128*64 bit的GDDRAM全部写入,在STM32-Flash中定义一个8*128的数组(表示8个page,每个page有128个column),每次要显示图片、数字等的时候直接写入这个STM32-Flash,再定时将这个数组的元素一次性写入SSD1306的GDDRAM,DMA就负责这个传输过程。
然而,SSD1306内部GDDRAM的读写指针变化的方式只能在一个page中增加column,不能自动的切换page。
这时我们就要将寻址的模式改位能自动切换到下一个page的模式,SSD1306给我们提供了这种模式的修改方法。
只需要发送20h命令,在修改成00即可,这就是为什么你发现我上节的初始化命令后面跟了个 0x20 0x00.
最终实现的刷新函数如下,可以使用定时器实现固定频率刷新,也可以在IIC的发送完成标志BTF的回调函数中自动调用,就能实现自动刷新。
void OLED_Refreash(void)
{
/* 里面会进行回调函数的具体赋值 */
HAL_I2C_Mem_Write_DMA(&hi2c1, OLED_IIC_ADDR, OLED_DATA, I2C_MEMADD_SIZE_8BIT, *OledFrameBuf, OLED_FRAMEBUF_LEN);
}
二、理顺IIC事件回调函数
在HAL_I2C_Mem_Write_DMA
中会对各类标志的回调函数进行注册,这里我感觉库文件写的好像有点问题,它对hi2c->hdmatx进行传输完成的回调函数注册,但是赋值的I2C_DMAXferCplt()
中却都是接收完成相关的弱定义函数。
——————————————————————————————————————————————————
那我觉得发送完成的回调函数应该叫HAL_I2C_MemTxCpltCallback()
,果不其然,搜了一下,还真有这个函数,它被I2C_MasterTransmit_BTF()
和I2C_MasterTransmit_TXE()
调用,这里又很混乱了,即然上面把Mem_Write与Master_Transmit分开,那这里为啥又混起来了呢?感觉HAL库整的有一点乱。
问题又来了,BTF和TXE这两个标志有什么区别?找到我们的STM32官方手册,BTF和TXE分别是 I2C_SR1 寄存器的 bit2 和 bit7 ,然而手册上写的解释我根本没看懂,寄存器为空和发送完字节都是什么意思?不懂。那就去看看源码吧,说不定能找到头绪。
先看I2C_MasterTransmit_TXE()
,下面截取了几个重要的片段,可以分析出TXE标志是每发送一个字节就产生,它控制着每个字节的发送过程。
而I2C_MasterTransmit_BTF()
则是对一次IIC通讯过程的结束进行处理:
到这里需要梳理一下,IIC一次传输完成后(也就是写入全部的GDDRAM后)会置 BTF 标志为1,此时会执行I2C_MasterTransmit_BTF()
,此函数完成标志位清除、状态改写的步骤后,会执行一个HAL_I2C_MemTxCpltCallback()
,这是一个弱定义的函数,用户可以自行实现。而我们要在一次GDDRAM传输完成后马上开启下一次传输实现自动刷新的目的,所以需要在HAL_I2C_MemTxCpltCallback()
中再次调用OLED_Refreash()
。
但是其实,我们还忘了,STM32并不是默认开启 IIC 各类事件的中断捕获,需要在STM32CubeMX中开启IIC的事件中断。