• LoRa网关项目——OLED(SSD1306)开发(一)


    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是支持一次发送多次命令/数据的:

    image-20210512124249540

    ​ 所以我们可以将初始化的命令序列组成一个数组(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# 位,默认与前面相同。

    image-20210512201748942

    ​ 正因为有这种机制,才需要调用HAL_I2C_Mem_Write_DMA(),使在发送SlaveAddr后能发送一个字节的控制命令(0x00或0x40)

    1. 0x00 ;Co 和 D/C# 均为0,表示接下来都是纯命令,不需要再携带Co 和 D/C#。

    2. 0x40;Co 为0, D/C# 为1,表示接下来都是纯数据,对GDDRAM的修改,也不需要再携带Co 和 D/C#。


    1.2 显示函数重写

    ​ 拿显示一个point来说,SSD1306的驱动中使用如下的流程:

    1. 定位需要操作的page
    2. 对该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。

    image-20210512144708881

    ​ 那我们是不是就能一次性将128*64 bit的GDDRAM全部写入,在STM32-Flash中定义一个8*128的数组(表示8个page,每个page有128个column),每次要显示图片、数字等的时候直接写入这个STM32-Flash,再定时将这个数组的元素一次性写入SSD1306的GDDRAM,DMA就负责这个传输过程。

    ​ 然而,SSD1306内部GDDRAM的读写指针变化的方式只能在一个page中增加column,不能自动的切换page。

    image-20210512150313570

    ​ 这时我们就要将寻址的模式改位能自动切换到下一个page的模式,SSD1306给我们提供了这种模式的修改方法。

    ​ 只需要发送20h命令,在修改成00即可,这就是为什么你发现我上节的初始化命令后面跟了个 0x20 0x00.

    image-20210512150537908 image-20210512150721905

    ​ 最终实现的刷新函数如下,可以使用定时器实现固定频率刷新,也可以在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()中却都是接收完成相关的弱定义函数。

    image-20210512151123700

    ——————————————————————————————————————————————————

    image-20210512153438003

    ​ 那我觉得发送完成的回调函数应该叫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标志是每发送一个字节就产生,它控制着每个字节的发送过程。

    image-20210512193037094 image-20210512193141206

    ​ 而I2C_MasterTransmit_BTF()则是对一次IIC通讯过程的结束进行处理:

    image-20210512193457256

    ​ 到这里需要梳理一下,IIC一次传输完成后(也就是写入全部的GDDRAM后)会置 BTF 标志为1,此时会执行I2C_MasterTransmit_BTF(),此函数完成标志位清除、状态改写的步骤后,会执行一个HAL_I2C_MemTxCpltCallback(),这是一个弱定义的函数,用户可以自行实现。而我们要在一次GDDRAM传输完成后马上开启下一次传输实现自动刷新的目的,所以需要在HAL_I2C_MemTxCpltCallback()中再次调用OLED_Refreash()

    ​ 但是其实,我们还忘了,STM32并不是默认开启 IIC 各类事件的中断捕获,需要在STM32CubeMX中开启IIC的事件中断。

    image-20210512195229054
  • 相关阅读:
    一周的前端面试
    PHP导出超大的CSV格式的Excel表方案
    Java HashMap Demo
    Vmware 设置桥接模式
    Vue 模板
    SpringMVC 拦截器
    IntelliJ IDEA 修改缓存文件设置
    Maven 命令操作项目
    Maven 介绍
    Spring Boot 5 SpringSecurity身份验证
  • 原文地址:https://www.cnblogs.com/Irvingcode/p/14761583.html
Copyright © 2020-2023  润新知