单片机-STM32部分:14、SPI

飞书文档https://x509p6c8to.feishu.cn/wiki/VYYnwOc9Zi6ibFk36lYcPQdRnlf

什么是****SPI

SPI 是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola(摩托罗拉)首先在其MC68HCXX系列处理器上定义的。

SPI,是一种高速的,全双工,同步的通信总线。

|-----------------------------------------------------------------|
| 高速的:通常可以达到几百kHz到几十MHz的范围。 全双工:支持全双工,可以同时收发 总线:支持一个主设备,一个或多个从设备。 |

SPI****主从模式

SPI分为主、从两种模式,一个SPI通讯系统需要包含一个(且只能是一个)主设备,一个或多个从设备。提供时钟的为主设备(Master),接收时钟的设备为从设备(Slave),SPI接口的读写操作,都是由主设备发起。当存在多个从设备时,通过各自的片选信号进行管理。

SPI****信号线

SPI接口一般使用四条信号线通信

MOSI(Master Output Slave Input): 主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。

MISO(Master Input Slave Output): 主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。

SCLK:串行时钟信号,由主设备产生。

CS/SS:从设备片选信号,由主设备控制。它的功能是用来作为"片选引脚",也就是选择指定的从设备,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。

UART、IIC、SPI的对比

|------|----------------------------------------------------|------------------------------------------------|-------------------------------------------------------|
| | UART | IIC | SPI |
| 通讯方式 | 异步 | 同步 | 同步 |
| 通讯线 | TXD 发送 RXD 接收 GND 地 | SDA 数据 SCL 时钟 | MOSI 主发从收 MISO 主收从发 SCK 时钟 CS 片选 |
| 设备从属 | 一对一 | 总线 | 总线 |
| 通讯速率 | 从几十Kbps到几Mbps | 标准模式下可达100kbps,快速模式下可达400kbps,高速模式下可达3.4Mbps | 几十Mbps甚至上百Mbps |
| 场景 | UART 常用于串行通信,如RS-232、RS-485通信,以及计算机与嵌入式设备间的通信。 | I²C 因其简洁的连线和地址机制,适用于板级设备间的通信,如传感器、EEPROM等。 | SPI 适用于短距离、高速数据传输,常见于传感器、屏幕、存储器(如Flash)与MCU之间的通信。 |

SPI****参数说明

SPI1 设置为全双工主模式,硬件 NSS 关闭

模式设置 : 全双工主机模式

  • 有主机模式全双工/半双工
  • 从机模式全双工/半双工
  • 只接收主机模式/只接收从机模式
  • 只发送主机模式

硬件 NSS(片选信号):Disable

可以选择使能,也可以使用其他IO口接到芯片的NSS上进行代替,如果只连接了一个从设备,可以不用开启片选。

其中SIP1的片选NSS : SPI1_NSS(PA4)

其中SIP2的片选NSS : SPI2_NSS(PB12)

如果片选引脚没有连接 SPI1_NSS(PA4)或者SPI2_NSS(PB12),则需要选择软件片选。

在stm32中,每个spi控制器的NSS信号引脚都具有两种功能,即输入和输出。所谓的输入就是NSS管脚的信号给自己。所谓的输出就是将NSS的信号送出去,给从机。

SPI 设置帧格式为摩托罗拉格式,数据长度 8Bits MSB 高位先传输,时钟分频 64 CPOL Low CPHA 为第一个边沿,关闭 CRC ,软件 NSS

**帧格式:**Motorla格式(摩托罗拉格式)目前只提供该格式,SPI标准协议就是由摩托罗拉设计的。

**数据长度:**8Bits

8Bits或16Bits,如果为16Bits,每次可以发送2Byte数据。

FirstBit **:**MSB先输出

MSB/LSB,通信中先传高位还是低位,和传输协议有关,主机从机保持一致即可。

**时钟分频:**分频为64分频

可见:SPI1 是在挂 APB2 上的, SPI2 是挂在 APB1 上的

当PCLK2为72M时,SPI速率为72/64=1.125M,在保证稳定情况下,STM32F1建议SPI不超过18M。

采样模式设置:

时钟极性CPOL是指SPI通讯设备处于空闲状态时,SCK信号线的电平,也就是通讯开始时SCK的电平。

时钟相位CPHA是指数据的采样的时刻。

|---------------------------------------------------------------------------------------------------------------|
| CPOL和CPHA的设置,决定SPI在什么时候进行采样,会影响读取到的数据。 根据时钟极性(CPOL)及相位(CPHA)不同,SPI有四种工作模式. 用的比较多的是模式:CPOL=0,CPHA=0,主机从机保持一致即可 |

|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| 时钟极性(CPOL)定义了时钟空闲状态电平: CPOL=0为时钟空闲时为低电平 CPOL=1为时钟空闲时为高电平 时钟相位(CPHA)定义数据的采集时间。 CPHA=0:在时钟的第一个跳变沿(上升沿或下降沿)进行数据采样。 CPHA=1:在时钟的第二个跳变沿(上升沿或下降沿)进行数据采样。 |

不开启 CRC 检验

NSS 为软件控制

我们用得更多的是由软件控制某些 GPIO引脚单独作为SS信号,这个GPIO引脚可以随便选择,像板卡中只有一个从设备,我们可以不使用片选引脚。如果我们把 NSS引脚配置为硬件自动控制,SPI模块能够自动判别它能否成为SPI的主机,或自动进入SPI从机模式。

生成代码

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 轮询: 最基本的发送接收函数,就是正常的发送数据和接收数据 HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout); *hspi: 选择SPI1/2,比如&hspi1,&hspi2 *pData : 需要发送的数据,如果设置为16bit,可以设置为数组 Size: 发送数据的字节数,1 就是发送一个字节数据 Timeout: 超时时间,就是执行发送函数最长的时间,超过该时间自动退出发送函数 成功返回HAL_OK HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout); *hspi: 选择SPI1/2,比如&hspi1,&hspi2 *pData : 接收发送过来的数据的数组 Size: 接收数据的字节数,1 就是接收一个字节数据 Timeout: 超时时间,就是执行接收函数最长的时间,超过该时间自动退出接收函数 成功返回HAL_OK HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout); SPI在发送数据的同时接收指定长度的数据 *hspi: 选择SPI1/2,比如&hspi1,&hspi2 pTxData:接收数据缓冲区首地址 pRxData:接收数据缓冲区首地址 size:要发送/接收数据的字节数 成功返回HAL_OK 中断相关接口: HAL_StatusTypeDef HAL_SPI_Transmit_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size); HAL_StatusTypeDef HAL_SPI_Receive_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size); HAL_StatusTypeDef HAL_SPI_TransmitReceive_IT(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size); DMA 相关接口: HAL_StatusTypeDef HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size); HAL_StatusTypeDef HAL_SPI_Receive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size); HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size); |

中断方式

中断模式和串口比较类似,但是SPI是总线通讯,一般由主机先在main.c中发送数据开启中断接收,并在中断回调函数中接收数据处理,处理完成后重新启动中断接收即可

复制代码
//main里启动中断
uint8_t sendData[2] = {1,2};
uint8_t receiveData[2];
HAL_SPI_TransmitReceive_IT(&hspi1, sendData, receiveData, 2);

//中断回调函数
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
  // 数据发送完成回调函数
  if (hspi == &hspi1)
  {
      HAL_SPI_TransmitReceive_IT(&hspi1, sendData, receiveData, 2);
  }
}

DMA 方式

添加SPI1的两个DMA通道,分别设置为Circular模式,传输的数据宽度要和SPI的数据位数相对应(spi是8位传输,这里就改为BYTE),设置DMA后,会默认开启SPI通道DMA中断。

复制代码
DMA频繁发送时,需检测是否传输完成
void DMA1_Channel3_IRQHandler(void)
{
  /* USER CODE BEGIN DMA1_Channel3_IRQn 0 */
  if(__HAL_DMA_GET_FLAG(&hdma_spi1_tx, DMA_FLAG_TC1)){
               
  }
  /* USER CODE END DMA1_Channel3_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_spi1_tx);
  /* USER CODE BEGIN DMA1_Channel3_IRQn 1 */
  /* USER CODE END DMA1_Channel3_IRQn 1 */
}

接收同理,可以在DMA接收传输完成后,才读出
void DMA1_Channel2_IRQHandler(void)
{
  /* USER CODE BEGIN DMA1_Channel2_IRQn 0 */
  if(__HAL_DMA_GET_FLAG(&hdma_spi1_rx, DMA_FLAG_TC1)){
               
  }
  /* USER CODE END DMA1_Channel2_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_spi1_rx);
  /* USER CODE BEGIN DMA1_Channel2_IRQn 1 */
  /* USER CODE END DMA1_Channel2_IRQn 1 */
}

同时,也可以结合SPI中断,完成各种特殊功能开发
void SPI1_IRQHandler(void)
{
  /* USER CODE BEGIN SPI1_IRQn 0 */
  if(__HAL_DMA_GET_FLAG(&hspi1, SPI_FLAG_TXE) == SET){
    发送缓冲区为空时,执行   
  }
  if(__HAL_DMA_GET_FLAG(&hspi1, SPI_FLAG_BSY) == RESET){
    SPI总线不忙时,执行
  }
  if(__HAL_DMA_GET_FLAG(&hspi1, SPI_SR_RXNE == SET){
    接收缓冲区不为空时,执行
  }
  /* USER CODE END SPI1_IRQn 0 */
  HAL_SPI_IRQHandler(&hspi1);
  /* USER CODE BEGIN SPI1_IRQn 1 */
  /* USER CODE END SPI1_IRQn 1 */
}

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DMA发送模式: 先调用HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size)发送 等待TXE=1 BSY=0时才能重新调用HAL_SPI_Transmit_DMA 当然,发送时也会产生中断void SPI1_IRQHandler() 发送完后会进入发送完成回调函数void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) 可以DMA发送再写进这里这样也可以实现循环发送,进入该回调函数就可以认为本次发送已经完成。 /** * @brief Tx Transfer completed callback. * @param hspi pointer to a SPI_HandleTypeDef structure that contains * the configuration information for SPI module. * @retval None */ __weak void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) |

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DMA接收模式: 先调用HAL_SPI_Receive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size) 检测到TCIF=1 DMA传输结束标志时,可以读取数据进行处理 接收时会产生中断void SPI1_IRQHandler() 接收完后会进入发送完成回调函数void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) /** * @brief Rx Transfer completed callback. * @param hspi pointer to a SPI_HandleTypeDef structure that contains * the configuration information for SPI module. * @retval None */ __weak void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) |

DMA 使用注意事项

  1. 1、要确保从机启动完成后,才开启主机DMA发送或查询,否则会出现丢帧
  2. 2、如果手动开关片选,要在DMA发送完成后才切换,否则会出现丢帧
  3. 3、如果用杜邦线连接,注意SPI速率高时,可能会丢包

SPI****驱动 1.3 OLED SH1106

https://www.waveshare.net/wiki/1.3inch_SH1106_OLED

|----------------------------------------------------------------------------|----------------------------------------------------------------------------|
| | |

注意:当选通 SPI 串口或者 I2C 接口,建议把 D7~D2 连接到 VDD1 或者 VSS。也允许把 D7~D2 悬空。

|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| 3线SPI和4线SPI,这里的4线SPI的意思是,在SPI的CS MOSI MISO CLK的基础上,多了一个D/C,指令/数据切换引脚,由于屏幕只需要接收主机发送的数据,主机不需要读取屏幕的数据,所以4线SPI的引脚为CS MOSI CLK D/C,3线则是CS MOSI CLK,少了一个D/C引脚。 |

4 线 SPI 的数据时序和对应 IO

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 从图中,我们可以知道IO部分分配如下: CS:片选信号 SI(D1):MOSI数据引脚 SCL(D0):时钟线 A0:指令/数据切换引脚 为什么没有MISO? 因为屏幕显示的时候,只需要从主机发送数据给屏幕,不需要读取屏幕的内容。 SPI 模式部分参数如下: MSB高位在前,CPOL=1 CPHA=1 时钟极性CPOL=1为时钟空闲时为高电平 CPOL=HIGH 时钟相位CPHA=1:在时钟的第二个跳变沿(上升沿或下降沿)进行数据采样。CPHA=2Edge 每 8 个时钟周期,A0 会被采样一次,移位寄存器的数据字节会写入到显示数据的 RAM(A0=1)或者命令寄存器(A0=0)中。 设置A0(DC)引脚为高时,写入显示数据 设置A0(DC)引脚为低时,写入指令数据(用于设置芯片显示模式、参数等) |

查看原理图

可以知道驱动屏幕的主要是4个IO,分别是

OLED_RES-复位信号-低电平复位-对应PB8

OLED_DC-数据/命令发送切换信号-对应PB4

OLED_SCLK-时钟信号-对应PB3

OLED_SDIN-数据信号-对应PB5

打开SPI3,模式为主机模式半双工,因为屏幕驱动只需要发送数据给屏幕显示,不需要读取屏幕信息。

因为SPI3的时钟和数据刚好对应PB3和PB5,这是板卡设计时选择好的。

此处需更新GPOL为Hight、GPHA为2Edge

修改IO的标签名称为OLED_CLK、OLED_DATA

设置PB8、PB4为输出模式

|-----------------------------------------------------|
| OLED_RES-复位信号-低电平复位-对应PB8 OLED_DC-数据/命令发送切换信号-对应PB4 |

修改标签名为OLED_DC、OLED_RES

最终添加USART1作为日志串口,方便调试

初始化屏幕

然后设置对应寄存器,这部分一般参考厂家的示例或手册。

复制代码
/* USER CODE BEGIN 0 */
static void sh1106_reset()
{
        //复位屏幕
        HAL_GPIO_WritePin(GPIOB, OLED_RES_Pin, GPIO_PIN_RESET);  //RES reset
        HAL_Delay(1000);
        //拉高复位引脚,进入正常工作模式
        HAL_GPIO_WritePin(GPIOB, OLED_RES_Pin, GPIO_PIN_SET);  //RES set
}
//发送指令
static void sh1106_write_cmd(uint8_t chData)
{
        HAL_GPIO_WritePin(GPIOB, OLED_DC_Pin, GPIO_PIN_RESET);//拉低DC,发送指令 
        HAL_SPI_Transmit(&hspi3, &chData, 1, 0xff);//发送
}      
//发送数据
static void sh1106_write_data(uint8_t chData)
{
        HAL_GPIO_WritePin(GPIOB, OLED_DC_Pin, GPIO_PIN_SET);  //拉高DC,发送数据 
        HAL_SPI_Transmit(&hspi3, &chData, 1, 0xff);//发送
}   

//初始化
void sh1106_init(void)
{      
        sh1106_reset();
        sh1106_write_cmd(0xAE);//--turn off oled panel
        sh1106_write_cmd(0x00);//---set low column address 00->02
        sh1106_write_cmd(0x10);//---set high column address
        sh1106_write_cmd(0x40);//--set start line address  Set Mapping RAM Display Start Line (0x00~0x3F)
        sh1106_write_cmd(0x81);//--set contrast control register
        sh1106_write_cmd(0xCF);// Set SEG Output Current Brightness
        sh1106_write_cmd(0xA1);//--Set SEG/Column Mapping    
        sh1106_write_cmd(0xC0);//Set COM/Row Scan Direction  
        sh1106_write_cmd(0xA6);//--set normal display
        sh1106_write_cmd(0xA8);//--set multiplex ratio(1 to 64)
        sh1106_write_cmd(0x3f);//--1/64 duty
        sh1106_write_cmd(0xD3);//-set display offset Shift Mapping RAM Counter (0x00~0x3F)
        sh1106_write_cmd(0x00);//-not offset
        sh1106_write_cmd(0xd5);//--set display clock divide ratio/oscillator frequency
        sh1106_write_cmd(0x80);//--set divide ratio, Set Clock as 100 Frames/Sec
        sh1106_write_cmd(0xD9);//--set pre-charge period
        sh1106_write_cmd(0xF1);//Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
        sh1106_write_cmd(0xDA);//--set com pins hardware configuration
        sh1106_write_cmd(0x12);
        sh1106_write_cmd(0xDB);//--set vcomh
        sh1106_write_cmd(0x40);//Set VCOM Deselect Level
        sh1106_write_cmd(0x20);//-Set Page Addressing Mode (0x00/0x01/0x02)
        sh1106_write_cmd(0x02);//
        sh1106_write_cmd(0x8D);//--set Charge Pump enable/disable
        sh1106_write_cmd(0x14);//--set(0x10) disable
        sh1106_write_cmd(0xA4);// Disable Entire Display On (0xa4/0xa5)
        sh1106_write_cmd(0xA6);// Disable Inverse Display On (0xa6/a7)
        sh1106_write_cmd(0xAF);//--turn on oled panel
}

发送清屏指令

OLED屏幕就是一个个小的有机自发光二极管组成的阵列,作为例子的屏幕的分辨率是128*64,即每行有128个发光二极管,一共有64行,如果我们需要显示一个图案,可以按图案的坐标点亮对应位置的发光二极管即可。

为了让你写代码可以更快找到需要点亮的发光二极管的位置,SH1106芯片提供了页寻址的方式。

|------------------------------------------------------------------------------------|
| 注意:SH1106最高支持点亮132*64分辨率的屏幕,而我们的屏幕分辨率是128*64,所以前2列和后2列是不需要用的,写入显示数据时,要注意设置起始列地址。 |

页是SH1106芯片设计者为了方便将同一列的8个点阵编成一组,用一个8bit数表示,这样132组个8bit被称为1页,这样一共有64/8=8页。

页寻找的方式

|-------|----------|----------|------|------------|-----------|
| | Column 0 | Column 2 | ... | Column 130 | Column131 |
| Page0 | --> | --> | --> | --> | --> |
| Page1 | --> | --> | --> | --> | --> |
| ... | | | | | |
| Page6 | | | | | |
| Page7 | | | | | |

选择对应的页 : 发送指令 0xB0-0xB7

我们可以看到D7-D4是固定的,D3-D0由对应页决定,总共有8页,分别是B0-B7。

|-------------------------------------------------------------------------------------------------------------|
| sh1106_write_cmd(0xB0); //设置页码为0xB0 sh1106_write_cmd(0xB1); //设置页码为0xB1 sh1106_write_cmd(0xB2); //设置页码为0xB2 |

设置列起始行地址

列地址由两个字节分别管理高低四位。D7 D6 D5 D4是固定的

发送起始列地址:发送指令0x00-0x0F,0x10-0x1F,下方是转换方法:

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 列地址由两个字节分别管理高低四位。 高四位:0b0001 A7 A6 A5 A4 低四位:0b0000 A3 A2 A1 A0 SH1106最高支持点亮132*64分辨率的屏幕,而我们的屏幕分辨率是128*64,所以前2列和后2列是不需要用的,写入显示数据时,要注意设置起始列地址。 所以起始地址为第二列: 0x02 A7 A6 A5 A4 A3 A2 A1 A0 0 0 0 0 0 0 1 0 高四位:0b00010000 = 0x10 低四位:0b00000010 = 0x02 sh1106_write_cmd(0x02); // 列起始地址低四位 sh1106_write_cmd(0x10); // 列起始地址高四位 |

从设定的页和列开始发送数据,列地址自动累加,页地址不会更新,如果超出范围则超出部分无效。

复制代码
/* USER CODE BEGIN 0 */
//优化前
void sh1106_clear_screen() 
{
    uint8_t Buffer1[128];
    sh1106_write_cmd(0xB0); //页码
    sh1106_write_cmd(0x02); //列起始地址低四位
    sh1106_write_cmd(0x10); //列起始地址高四位
    for (j = 0; j < 128; j ++) {
        Buffer1[j] = 0; //填充0
        sh1106_write_data(Buffer1[j]); //发送数据,每次发送1byte,8bit
    }
    uint8_t Buffer2[128];
    sh1106_write_cmd(0xB1); //页码
    sh1106_write_cmd(0x02); //列起始地址低四位
    sh1106_write_cmd(0x10); //列起始地址高四位
    for (j = 0; j < 128; j ++) {
        Buffer2[j] = 0; //填充0
        sh1106_write_data(Buffer2[j]); //发送数据,每次发送1byte,8bit
    }
    xxx
}

//优化后

uint8_t s_chDispalyBuffer[128][8]; //8*8bit=64

void sh1106_clear_screen() 
{
        uint8_t i, j;
        for (i = 0; i < 8; i ++) {
                sh1106_write_cmd(0xB0 + i); //设置页码从0xB0开始到0xB7
                sh1106_write_cmd(0x02); //列起始地址低四位
                sh1106_write_cmd(0x10); //列起始地址高四位
                //8*128个点,全部清零
                for (j = 0; j < 128; j ++) {
                    s_chDispalyBuffer[j][i] = 0; //填充0
                    sh1106_write_data(s_chDispalyBuffer[j][i]); //发送数据
                }
        }
}

画点函数

复制代码
/* USER CODE BEGIN 0 */
/**
  把需要点亮的点转换为显示数组s_chDispalyBuffer的一个bit状态
  chXpos: 绘制点的x坐标 0<= x <=127
  chYpos: 绘制点的y坐标 0<= 7 <=63
  chPoint: 0: 熄灭  1: 点亮
**/
void sh1106_draw_point(uint8_t chXpos, uint8_t chYpos, uint8_t chPoint)
{
        uint8_t chPos, chBx, chTemp = 0;
       
        if (chXpos > 127 || chYpos > 63) {
                return;
        }
        //chYpos坐标转换,因为我们用8个字节管理了64个bit,所以需要把y坐标转换到对应的字节bit位置
        chPos = 7 - chYpos / 8;   //找出那一页
        chBx = chYpos % 8;        //找出哪一位
        chTemp = 1 << (7 - chBx); //把对应位置1
        if (chPoint) {
            s_chDispalyBuffer[chXpos][chPos] |= chTemp;
        } else {
            s_chDispalyBuffer[chXpos][chPos] &= ~chTemp;
        }
        sh1106_refresh_gram();
}

/**
所有页更新到屏幕显示
把显示数组s_chDispalyBuffer发送到屏幕显示
**/
void sh1106_refresh_gram(void)
{
        uint8_t i, j;
        for (i = 0; i < 8; i ++) { 
                sh1106_write_cmd(0xB0 + i); //设置页码从0xB0开始到0xB7  
                sh1106_write_cmd(0x02); //列起始地址低四位
                sh1106_write_cmd(0x10); //列起始地址高四位     
                for (j = 0; j < 128; j ++) {
                        sh1106_write_data(s_chDispalyBuffer[j][i]);
                }
        }  
}

显示图像

下方图像数组,我们通过取模软件可以生成

字库取模

中文取模时,需要一个一个字取,顺向取模,根据生成的.c内容记录字体大小,通过画图的方式绘制到屏幕。

图片取模

复制代码
const uint8_t c_chSingal816[16] = //mobie singal 16*8
{
        0xFE,0x02,0x92,0x0A,0x54,0x2A,0x38,0xAA,0x12,0xAA,0x12,0xAA,0x12,0xAA,0x12,0xAA
};

const uint8_t c_chMsg816[16] =  //message 16*8
{
        0x1F,0xF8,0x10,0x08,0x18,0x18,0x14,0x28,0x13,0xC8,0x10,0x08,0x10,0x08,0x1F,0xF8
};

const uint8_t c_chBat816[16] = //batery 16*8
{
        0x0F,0xFE,0x30,0x02,0x26,0xDA,0x26,0xDA,0x26,0xDA,0x26,0xDA,0x30,0x02,0x0F,0xFE
};

void sh1106_draw_bitmap(uint8_t chXpos, uint8_t chYpos, const uint8_t *pchBmp, uint8_t chWidth, uint8_t chHeight)
{
    uint16_t i, j, byteWidth = (chWidth + 7) / 8;
    //遍历图片的宽高,取出每一点,判断为1的位,为需要点亮的点,通过画点函数绘制到屏幕
    for(j = 0; j < chHeight; j ++){
        for(i = 0; i < chWidth; i ++ ) {
            if(*(pchBmp + j * byteWidth + i / 8) & (128 >> (i & 7))) {
                sh1106_draw_point(chXpos + i, chYpos + j, 1);
            }
        }
    }
}

最终在****main 函数中使用

复制代码
/* USER CODE BEGIN Includes */
#include <stdio.h>

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_SPI3_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  printf("app init \n");
  sh1106_init();
  /* USER CODE END 2 */

  while (1)
  {
    /* USER CODE BEGIN 3 */
    sh1106_clear_screen();
    sh1106_draw_point(10,10,1);
    HAL_Delay(1000);
    sh1106_clear_screen();
    //起始坐标(0,2) 绘制的图标数组c_chSingal816, 图标的宽高(18,8)
    sh1106_draw_bitmap(0, 2, c_chSingal816, 16, 8);
    HAL_Delay(1000);
  }
  /* USER CODE END 3 */
}


/* USER CODE BEGIN 4 */
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1 , (uint8_t *)&ch, 1, 0xFFFF);
    return ch;
}
/* USER CODE END 4 */
相关推荐
Yesheldon1 小时前
Cadence 高速系统设计流程及工具使用三
嵌入式硬件·fpga开发·硬件架构·硬件工程·智能硬件
inputA2 小时前
【LwIP源码学习6】UDP部分源码分析
c语言·stm32·单片机·嵌入式硬件·网络协议·学习·udp
思考的味道3 小时前
SVM在医疗设备故障维修服务决策中的应用:策略、技术与实践
嵌入式硬件
真的想上岸啊3 小时前
学习51单片机01(安装开发环境)
嵌入式硬件·学习·51单片机
7yewh4 小时前
MCU程序加密保护(二)ID 验证法 加密与解密
单片机·嵌入式硬件·安全
YOYO--小天4 小时前
RS485和RS232 通信配置
linux·嵌入式硬件
小_楠_天_问4 小时前
第二课:ESP32 使用 PWM 渐变控制——实现模拟呼吸灯或音调变化
c语言·嵌入式硬件·mcu·esp32·arduino·pwm·esp32-s3
欢乐熊嵌入式编程5 小时前
智能手表项目风险评估与应对计划书
嵌入式硬件·物联网·目标跟踪·智能手表
JANYI20186 小时前
TTL、RS-232、RS-485电平转换详解
单片机·嵌入式硬件
平凡灵感码头6 小时前
基于智能家居项目 解析DHT11温湿度传感器
单片机·智能家居