第13章_瑞萨MCU零基础入门系列教程之Common SPI

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id=728461040949

配套资料获取:https://renesas-docs.100ask.net

瑞萨MCU零基础入门系列教程汇总: https://blog.csdn.net/qq_35181236/article/details/132779862


第13章 Common SPI

本章目标

  • 使用RASC快速配置Common SPI模块
  • 学会使用Common SPI的API进行数据收发

13.1 Common SPI模块的使用

RA芯片的SPI分为Simple SPI和Common SPI。Simple SPI就是本书《第8章 SCI SPI》所讲的SCI模块的SPI模式,是使用串行总线来模拟SPI协议,而本章所讲的是芯片内部实际存在的一个硬件SPI控制器模块。

本章将会从Common SPI在RASC中的配置开始讲解,最后编写程序操作OLED(它的驱动芯片SSD1306),重点在于学习使用Common SPI的API。

13.1.1 配置SPI模块

本章实验驱动的OLED模块非板载模块,而是一个外接模块,因而选择的Common SPI通道可以任选一路。本书选用的是SPI0,使用的GPIO引脚如下:

序号模块引脚芯片SPI0引脚
1CSP205
2SCKP204
3MISOP202
4MOSIP203
5GPIOP206

其中CS是SPI的片选信号引脚,GPIO则是OLED灯驱动芯片SSD1306在4线SPI通信下切换数据和命令的控制信号引脚(即决定SPI总线上传输的数据,是控制SSD1306的指令还是写入SSD1306显存的数据)。

配置引脚CS和GPIO为普通输出功能,参考前文《5.3 LED实验》配置GPIO。要在RASC中配置Common SPI,首先在RASC的“Pin Configuration”中的“Peripherals”里展开“Connectivity:SPI”,选中里面的SPI0,如下图所示:

在第5步配置SPI0时,对于“Pin Group Selection”,有4组可以选:Mixed/A/B/C_Only,如图所示:

默认是“A_only”,不同的组别支持的引脚是不同的,此处根据硬件选择的引脚决定,本章使用的是默认引脚A组的P202/P203/P204。

选好IO的组别后,将“Operation Mode”从“Disabled”改为“Enabled”。操作模式里的另一个选项“Custom”的作用是:Common SPI下的所有引脚都可以由用户自己选择;而选择“Enabled”时对应的时钟线和数据线时固定的引脚;片选引脚可以由用户自己选择。本书使用Enabled默认的数据线和时钟线,且不会使用硬件片选,而使用软件控制IO来控制片选信号。

所以Common SPI的SPI0的模式和引脚配置结果如下图所示:

配置好SPI0的模式和引脚后,就要去RASC的“Stacks”处增加SPI0的Stack模块。在“Stacks”配置界面点击“New Stack”,选择其中的“Connectivity”,再选择里面的“SPI(r_spi)”,此处一定要注意不是选择“SPI(r_sci_spi)”。如下图所示:

当新建好一个SPI Stack后,需要去它的“属性”中配置详细的参数。本书根据SPI协议,将SPI Stack的属性分成了8个较为重要的可配置参数,如下图所示:

① SPI模块的名称和使用通道:建议名称和通道匹配,例如本章使用的是SPI0,那么通道channel就是0,而名称则对应的是g_spi0;以后使用FSP的API操作SPI0时就可以g_spi0.papi->xxxx()来使用SPI0模块;
② 中断优先级设置:此处设置收发完成的中断优先级和错误中断优先级,需要综合考虑工程中其它外设的中断优先级设置,达到更好的系统控制效果;
③ SPI的操作模式:Master或Slave,这里选择Master;
④ SPI的时钟特性:设置SPI空闲时时钟线呈现什么电平、传输过程中在时钟的哪个边沿采样数据;
⑤ 数据位格式:可以选择高位在前或低位在前,本书使用的设备均为高位在前的模式;
⑥ 中断回调函数:建议函数名称和通道匹配;
⑦ SPI的操作模式:全双工/半双工,时钟同步或异步等;
⑧ 通信速率:本书使用的处理器在最大主频200MHz下,SPI的通信速率最大允许50MHz,通信速率需要考虑从机支持的最大通信速率来设置。

13.1.2 配置信息解读

使用RASC配置Common SPI后,生成了两类配置信息:Common SPI对应的引脚配置信息、Common SPI本身的配置信息。以本章的实验“1301_spi_oled”为例。

  1. 引脚配置信息

该信息会在1301_spi_oled\ra_gen\pin_data.c文件里生成。在RASC里配置的每一个引脚,都会在pin_data.c生成一个ioport_pin_cfg_t数组项,里面的内容跟配置时选择的参数一致。代码如下:

const ioport_pin_cfg_t g_bsp_pin_cfg_data[] =
{......(省略内容){ .pin = BSP_IO_PORT_02_PIN_02, /* MISO */.pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN| (uint32_t) IOPORT_PERIPHERAL_SPI) },{ .pin = BSP_IO_PORT_02_PIN_03, /* MOSI */.pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN| (uint32_t) IOPORT_PERIPHERAL_SPI) },{ .pin = BSP_IO_PORT_02_PIN_04, /* SCK */.pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN| (uint32_t) IOPORT_PERIPHERAL_SPI) },{ .pin = BSP_IO_PORT_02_PIN_05, /* CS */.pin_cfg = ((uint32_t) IOPORT_CFG_DRIVE_HIGH| (uint32_t) IOPORT_CFG_PORT_DIRECTION_OUTPUT | (uint32_t) IOPORT_CFG_PORT_OUTPUT_HIGH) },{ .pin = BSP_IO_PORT_02_PIN_06, /* GPIO */.pin_cfg = ((uint32_t) IOPORT_CFG_DRIVE_MID| (uint32_t) IOPORT_CFG_PORT_DIRECTION_OUTPUT | (uint32_t) IOPORT_CFG_PORT_OUTPUT_HIGH) },......(省略内容)};
  • 第04~12行:SPI0的GPIO配置为外设复用模式;
  • 第13~16行:SPI0的片选CS引脚配置为输出模式,默认输出高电平;
  • 第17~20行:外接OLED模块的数据/命令切换引脚,配置为输出模式,默认输出高电平;
  1. SPI配置信息

SPI模块信息会在1301_spi_oled\ra_gen\hal_data.c文件里生成。在RASC里配置的SPI通道、时钟特性和中断回调函数函数等,都会在hal_data.c生成一个spi_cfg_t结构体,里面的内容跟配置时选择的参数一致。代码如下:

/** SPI configuration for SPI HAL driver */
const spi_cfg_t g_spi0_cfg =
{ .channel = 0,......(省略内容).operating_mode = SPI_MODE_MASTER,.clk_phase = SPI_CLK_PHASE_EDGE_ODD,.clk_polarity = SPI_CLK_POLARITY_LOW,.mode_fault = SPI_MODE_FAULT_ERROR_DISABLE,.bit_order = SPI_BIT_ORDER_MSB_FIRST,.p_transfer_tx = g_spi0_P_TRANSFER_TX,.p_transfer_rx = g_spi0_P_TRANSFER_RX,.p_callback = spi0_callback,.p_context = NULL,.p_extend = (void*) &g_spi0_ext_cfg, };
  • 第05行:操作模式为主机模式;
  • 第06~07行:时钟特性是在第1个边沿触发数据采样,空闲时时钟线为低电平;
  • 第10~11行:绑定spi0的收发函数;
  • 第12行:注册中断回调函数;
  • 第14行:配置扩展参数;

扩展参数中的值来自SPI0的配置参数,它对应SPI0的某些寄存器的值。例如通信速率是50MHz时,那么扩展参数中寄存器SPBR和BRDV的值将由RASC计算出来。SPI0的扩展配置代码如下:

/** SPI extended configuration for SPI HAL driver */
const spi_extended_cfg_t g_spi0_ext_cfg =
{ .spi_clksyn = SPI_SSL_MODE_CLK_SYN,.spi_comm = SPI_COMMUNICATION_FULL_DUPLEX,.ssl_polarity = SPI_SSLP_LOW,.ssl_select = SPI_SSL_SELECT_SSL0,.mosi_idle = SPI_MOSI_IDLE_VALUE_FIXING_DISABLE,.parity = SPI_PARITY_MODE_DISABLE,.byte_swap = SPI_BYTE_SWAP_DISABLE,.spck_div ={/* Actual calculated bitrate: 50000000. */.spbr = 0,.brdv = 0 },.spck_delay = SPI_DELAY_COUNT_1,.ssl_negation_delay = SPI_DELAY_COUNT_1,.next_access_delay = SPI_DELAY_COUNT_1 };

这个结构体有助于开发者了解SPI的通信细节,比如每个时钟采样或发送数据时的时延变化等。对于深度开发SPI外设的用户会比较有用。

13.1.3 中断回调函数

中断回调函数的原型已经在hal_data.h中进行了声明,需要用户在自己的程序中实现,其原型代码如下:

#ifndef spi0_callback
void spi0_callback(spi_callback_args_t *p_args);
#endif

其参数类型是spi_callback_args_t结构体指针,此结构体会表明触发中断的是哪一个SPI通道,触发的事件是什么,该结构体的详细代码如下:

/** Common callback parameter definition */
typedef struct st_spi_callback_args
{uint32_t     channel;              ///< Device channel numberspi_event_t  event;                ///< Event codevoid const * p_context;            ///< Context provided to user during callback
} spi_callback_args_t;
  • 第04行:表明SPI通道;
  • 第05行:表明触发事件类型;

触发事件类型是一个枚举类型结构体,包括但不限于发送完成事件、发送中止等,枚举代码如下:

/** SPI events */
typedef enum e_spi_event
{SPI_EVENT_TRANSFER_COMPLETE = 1,   ///< The data transfer was completedSPI_EVENT_TRANSFER_ABORTED,        ///< The data transfer was abortedSPI_EVENT_ERR_MODE_FAULT,          ///< Mode fault errorSPI_EVENT_ERR_READ_OVERFLOW,       ///< Read overflow errorSPI_EVENT_ERR_PARITY,              ///< Parity errorSPI_EVENT_ERR_OVERRUN,             ///< Overrun errorSPI_EVENT_ERR_FRAMING,             ///< Framing errorSPI_EVENT_ERR_MODE_UNDERRUN        ///< Underrun error
} spi_event_t;

比如本章的实验就是在中断回调函数中,根据通道值和触发事件来判断SPI传输是否完成。如果传输完成,则将一个标志位设置为true。参考代码如下:

void spi0_callback(spi_callback_args_t *p_args)
{if(p_args->channel == 0){if(p_args->event == SPI_EVENT_TRANSFER_COMPLETE)gSPITxCplt = true;}
}
static bool SPIWaitTxCplt(void)
{unsigned short wTimeout = 100;while(!gSPITxCplt && wTimeout){HAL_Delay(1);wTimeout--;}gSPITxCplt = false;if(gSPITxCplt==false && wTimeout==0)return false;return true;
}
  • 第03~07行:根据通道和触发事件的值来设置通讯标志位;
  • 第10~22行:根据通讯标志为封装一个通讯超时等待函数;

13.1.4 API接口及其用法

在路径1301_spi_oled/ra/fsp/inc/api/r_spi_api.h中定义了SPI模块的接口,它定义了一个结构体类型spi_api_t,内容如下:

/** Shared Interface definition for SPI */
typedef struct st_spi_api
{fsp_err_t (* open)(spi_ctrl_t * p_ctrl, spi_cfg_t const * const p_cfg);fsp_err_t (* read)(spi_ctrl_t * const p_ctrl, void * p_dest, uint32_t const length,spi_bit_width_t const bit_width);fsp_err_t (* write)(spi_ctrl_t * const p_ctrl, void const * p_src,uint32_t const length,spi_bit_width_t const bit_width);fsp_err_t (* writeRead)(spi_ctrl_t * const p_ctrl, void const * p_src, void * p_dest, uint32_t const length,spi_bit_width_t const bit_width);fsp_err_t (* callbackSet)(spi_ctrl_t * const p_api_ctrl, void (* p_callback)(spi_callback_args_t *),void const * const p_context, spi_callback_args_t * const p_callback_memory);fsp_err_t (* close)(spi_ctrl_t * const p_ctrl);
} spi_api_t;

在具体的C文件中,需要定义一个spi_api_t结构体变量,比如在r_spi.c里定义了如下结构体变量:

/* SPI implementation of SPI interface. */
const spi_api_t g_spi_on_spi =
{.open        = R_SPI_Open,.read        = R_SPI_Read,.write       = R_SPI_Write,.writeRead   = R_SPI_WriteRead,.close       = R_SPI_Close,.callbackSet = R_SPI_CallbackSet
};

要使用SPI的控制接口,可以调用结构体g_spi_on_spi里的各个函数指针,也可以直接调用r_spi.c里实现的各个函数。

  1. 打开SPI设备

打开SPI设备的函数原型如下:

fsp_err_t (* open)(spi_ctrl_t * p_ctrl, spi_cfg_t const * const p_cfg);

它有两个参数:

  • spi_ctrl_t*:一个void类型的指针类型,指向SPI的控制参数结构体spi_instance_ctrl_t,该结构体会在hal_data.h中定义一个全局变量g_spi0_ctrl,结构体原型如下:
typedef struct st_spi_instance_ctrl
{uint32_t          open;            ///< Indicates whether the open() API has been successfully called.spi_cfg_t const * p_cfg;           ///< Pointer to instance configurationR_SPI0_Type     * p_regs;          ///< Base register for this channelvoid const      * p_tx_data;       ///< Buffer to transmitvoid            * p_rx_data;       ///< Buffer to receiveuint32_t          tx_count;        ///< Number of Data Frames to transfer (8-bit, 16-bit, 32-bit)uint32_t          rx_count;        ///< Number of Data Frames to transfer (8-bit, 16-bit, 32-bit)uint32_t          count;           ///< Number of Data Frames to transfer (8-bit, 16-bit, 32-bit)spi_bit_width_t   bit_width;       ///< Bits per Data frame (8-bit, 16-bit, 32-bit)/* Pointer to callback and optional working memory */void (* p_callback)(spi_callback_args_t *);spi_callback_args_t * p_callback_memory;/* Pointer to context to be passed into callback function */void const * p_context;
} spi_instance_ctrl_t;

此结构体会记录SPI通信过程中的参数变化,比如收发数据存储地址、收发数据剩余个数、收发数据位宽等;

  • spi_cfg_t*:spi配置参数结构体,指向SPI的配置参数结构体,该结构体会在hal_data.h中定义一个全局变量g_spi0_cfg,该结构体的原型如下:
typedef struct st_spi_cfg
{uint8_t channel;                                   ///< Channel number to be usedIRQn_Type                   rxi_irq;          ///< Receive Buffer Full IRQ numberIRQn_Type                   txi_irq;       ///< Transmit Buffer Empty IRQ numberIRQn_Type                   tei_irq;        ///< Transfer Complete IRQ numberIRQn_Type                   eri_irq;               ///< Error IRQ numberuint8_t                     rxi_ipl;      ///< Receive Interrupt priorityuint8_t                     txi_ipl;       ///< Transmit Interrupt priorityuint8_t         tei_ipl;      ///< Transfer Complete Interrupt priorityuint8_t                     eri_ipl;               ///< Error Interrupt priorityspi_mode_t        operating_mode;   ///< Select master or slave operating modespi_clk_phase_t        clk_phase;    ///< Data sampling on odd or even clock edgespi_clk_polarity_t          clk_polarity;          ///< Clock level when idlespi_mode_fault_t            mode_fault;            ///< Mode fault error (master/slave conflict) flagspi_bit_order_t             bit_order;      ///< Select to transmit MSB/LSB firsttransfer_instance_t const * p_transfer_tx;         ///< To use SPI DTC/DMA write transfer, link a DTC/DMA instance here.  Set to NULL if unused.transfer_instance_t const * p_transfer_rx;         ///< To use SPI DTC/DMA read transfer, link a DTC/DMA instance here.  Set to NULL if unused.void (* p_callback)(spi_callback_args_t * p_args); ///< Pointer to user callback functionvoid const * p_context;   ///< User defined context passed to callback functionvoid const * p_extend;      ///< Extended SPI hardware dependent configuration
} spi_cfg_t;

此结构体会指明SPI通信的中断类型、时钟特性和中断回调函数等。

这两个结构体的配置都由RASC自动生成,不需要用户手动配置。开发者可以参考如下代码来打开某个SPI设备完成初始化:

fsp_err_t err = g_spi0.p_api->open(g_spi0.p_ctrl, g_spi0.p_cfg);
if(FSP_SUCCESS != err)
{printf("Error when open spi0 device!\r\n");return -1;
}
printf("Success to open device: spi0\r\n");
  1. 关闭SPI设备

关闭SPI设备的函数原型如下:

fsp_err_t (* close)(spi_ctrl_t * const p_ctrl);

此函数会将控制结构体中的open成员设置为false,表明SPI设备处于关闭状态,如果使能了SPI的参数校验功能,则在open为false的情况下,读写函数直接返回错误。

  1. SPI读取数据

SPI读取数据的函数原型如下:

fsp_err_t (* read)(spi_ctrl_t * const p_ctrl, void * p_dest, uint32_t const length,spi_bit_width_t const bit_width);
  • p_dest:目的数据(用来接收数据)的地址;
  • length:读取数据的个数
  • bit_width:读取数据的宽度,支持3bit~31bit;

读取到的数据个数换算:count=length*bit_width/8 bytes。

  1. SPI发送数据

发送数据的函数原型如下:

fsp_err_t (* write)(spi_ctrl_t * const p_ctrl, void const * p_src,uint32_t const length,spi_bit_width_t const bit_width);
  • p_src:源数据(要发送的数据)地址;
  • length:发送数据的个数;
  • bit_width:发送数据的位宽,支持3bit~31bit;

发送的数据个数换算:count=length*bit_width/8 bytes。开发者可以参考如下代码来发送n个字节的数据:

fsp_err_t err = g_spi0.p_api->write(g_spi0.p_ctrl, (uint8_t*)rbuf, wSize, SPI_BIT_WIDTH_8_BITS);
if(FSP_SUCCESS != err)printf("Function:%s\tLine:%d\r\n", __FUNCTION__, __LINE__);
  1. SPI收发数据

SPI传输是全双工的。即使只调用read函数想读取N字节数据,它也会发出N个字(只不过我们忽略了这些发送出去的数据,默认为0xFF);即使你只调用write函数想发出N字节数据,它也会接收到N字节数据(只不过丢弃了而已)。需要同时读、写时,可以使用SPI的同时收发函数,原型如下:

fsp_err_t (* writeRead)(spi_ctrl_t * const p_ctrl, void const * p_src, void * p_dest, uint32_t const length,spi_bit_width_t const bit_width);
  • p_src:源数据(要发送的数据)地址;
  • p_dest:目的数据(用来接收数据)的地址;
  • length:收发数据的个数;
  • bit_width:收发数据的位宽;

13.2 SPI驱动OLED显示实验

13.2.1 硬件连接

本书使用的外接SPI OLED模块是百问网的SPI OLED模块,外形如下图:

和处理器的连线如下表所示:

序号模块引脚芯片SPI0引脚
1CSP205
2SCKP204
3MISOP202
4MOSIP203
5GPIOP206
6GNDGND
73V33V3

13.2.2 SSD1306驱动解析

SSD1306是一个分辨率为12864的单色显示器,也就意味着如果要全屏刷新这个显示器的话,在处理器的内存映射中需要一个12864/8字节的内存来表示显示器的显存。

驱动SSD1306需要从以下几个角度来分析:

  • 支持的接口协议;
  • 显存写入模式;
  • 显示区域设置;
  • 显示模式设置;
  1. 接口协议

SSD1306支持6800/8080并行接口协议,也支持3线制和4线制的SPI接口协议,还支持I2C接口协议。本书使用的模块是4线制SPI接口协议。

对于4线制SPI接口协议,其通信过程对于片选信号和数据/命令切换信号的要求如下图:

要启动SPI传输,需要设置CS为低电平以选中目标设备。而DC#信号被用来分辨所传输的SPI数据是“命令”,还是“显存数据”。

SSD1306的4线制SPI通信的时序如下图所示:

可以看到,对于SSD1306而言,它会在时钟信号的低电平期间切换数据,在上升沿采样数据,在时钟信号的高电平期间保持数据,传输的数据都是高位在前低位在后。因而在使用RASC配置SPI的时候就要和这个时钟特性要匹配。

需要注意的是,对于SSD1306而言,在串行通行模式(SPI或I2C)下是没有读取数据的操作,只有写操作。

  1. 显存写入模式

SSD1306的显存横向纵向分布方式不同。横向分布下,1个bit表示一个地址值,叫做column地址;纵向分布下,8个bit表示一个地址值,叫做页地址。SSD1306纵向共有64bit,分为8个页。

SSD1306的显存写入模式就是它的地址模式,分为:页地址模式、横向地址模式和纵向地址模式。不同的地址模式下,把数据写入显存后,显存的地址变化方式是不一样的。

在页地址模式下,如果往page0的column0写入一个byte的数据,那么column将会从0递增为1;当column递增到横向地址的最大值(满屏为127)时,column会回到0,而页地址page不会变化依然保存为0。因而,在页地址模式下要想将全屏刷新,需要分8次操作:先设置Paga n地址,写入128个字节(n的 取值从0到7)。

在横向地址模式下,如果往page0的column写入一个byte的数据,那么column将会从0递增为1;当column递增到横向地址的最大值时,column会回到0,而页地址会递增1;如果页地址递增到最大页数,则将回到第0页开始覆盖写。

在纵向地址模式下,如果往page0的column0写入一个byte的数据,那么column地址不会变化,而页地址page将会从0递增1,当page递增到最大页地址时,page会回到0地址,而column地址会递增1。

在横向地址模式或纵向地址模式下,要刷新全屏的数据,只需要在程序中执行一次发送128*8 bytes的数据到SSD1306即可,地址会自动变化,相比于页地址模式会更方便。

  1. 显示区域设置

设置显示区域有两种方法:硬件映射和命令控制。

对于硬件映射,是使用0x400x7F区间的指令,告诉SSD1306从硬件的063行的哪一行开始显示。这个方法多用“裁剪应用”,不是很常用。

而命令控制则是通过设置column起始地址和结束地址以及page页的起始地址和结束地址来划定一个显示范围,在小范围更新图像数据时比较好用(全屏刷新时不用每次都设置显示区域)。

设置column地址和页地址的方法如下图:

  • 设置column地址的命令是0x21,然后发送2个字节的数据指明起始地址和结束地址;
  • 设置page地址的命令是0x22,然后发送2个字节的数据指明起始地址和结束地址;
  1. 显示模式设置

显示模式设置的参数比较多,比如:显存中每一位数据对应一个像素,那么这位数据是1还是0才能让像素被点亮?显示像素是从左往右显示,还是从右往左显示等等。

13.2.3 SSD1036驱动程序

  1. 中断回调函数和通讯等待函数

这两个函数在前文《13.1.3 中断回调函数》已经见过,此处不再展示。

  1. 信号引脚控制函数

驱动SSD1306时,需要控制CS和DCX两个引脚,因而需要初始化IO且各自封装一个API供后续使用,代码如下:

static void OLEDDrvGpioInit(void)
{fsp_err_t err = g_ioport.p_api->open(g_ioport.p_ctrl, g_ioport.p_cfg);if(FSP_SUCCESS != err)printf("Failed to open GPIOs!\r\n");
}
static void OLEDDrvWriteCS(CS eState)
{fsp_err_t err = g_ioport.p_api->pinWrite(g_ioport.p_ctrl, BSP_IO_PORT_02_PIN_05, (bsp_io_level_t)eState);if(FSP_SUCCESS != err)printf("Failed to write %d to pin 0x%.4x!\r\n", eState, BSP_IO_PORT_02_PIN_05);
}
static void OLEDDrvWriteDCX(DCX eState)
{fsp_err_t err = g_ioport.p_api->pinWrite(g_ioport.p_ctrl, BSP_IO_PORT_02_PIN_06, (bsp_io_level_t)eState);if(FSP_SUCCESS != err)printf("Failed to write %d to pin 0x%.4x!\r\n", eState, BSP_IO_PORT_02_PIN_06);
}
  1. 显示设备对象封装和注册

本章的实验还是使用《8.3 sci spi驱动显示屏实验》节的设备对象封装结构体来表示一个显示设备:

typedef struct DisplayDevice {char *name;void *FBBase; /* CPU能直接读写的显存 */unsigned short wXres;    /* X方向分辨率 */unsigned short wYres;    /* Y方向分辨率 */unsigned short wBpp;     /* 每个像素使用多少个像素 */unsigned int   dwSize;int            (*Init)(struct DisplayDevice *ptDev);   /* 硬件初始化 */void           (*DisplayON)(struct DisplayDevice *ptDev);   /* 开启显示 */void           (*DisplayOFF)(struct DisplayDevice *ptDev);   /* 关闭显示 */void           (*SetDisplayWindow)(struct DisplayDevice* ptDev, \unsigned short wXs, unsigned short wYs, \unsigned short wXe, unsigned short wYe);void           (*Flush)(struct DisplayDevice *ptDev); /* 把FBBase的数据刷到LCD的显存里 *//* 设置FBBase中的数据, 把(iX,iY)的像素设置为颜色dwColor* dwColor的格式:0x00RRGGBB*/int          (*SetPixel)(struct DisplayDevice *ptDev, \unsigned short wX, unsigned short wY, \unsigned short wColor);struct DisplayDevice *pNext;
}DisplayDevice, *PDisplayDevice;

上述代码是放在drv_disp.h中,还需要在drv_oled.c中实现DisplayDevice结构体,代码如下:

static unsigned short gOledFbuf[128*64/8];
static DisplayDevice gOledDev = {.name = "OLED",.FBBase = gOledFbuf,.wXres = 128,.wYres = 64,.wBpp = 1,.dwSize = 128*64*1/8,.Init = OLEDDrvinit,.DisplayON = OLEDDrvDispON,.DisplayOFF = OLEDDrvDispOFF,.SetDisplayWindow = OLEDDrvSetDispWindow,.Flush = OLEDDrvFulsh,.SetPixel = OLEDDrvSetPixel
};

并且在drv_oled.c中写一个设备获取接口将OLED显示设备提供给上层应用:

struct DisplayDevice *OLEDGetDevice(void)
{return &gOledDev;
}

此函数会在drv_disp.h中进行声明。

显示设备的函数指针所指向的函数将会在后文列举说明。

  1. 发送指令函数

发送指令时需要将DCX拉低,可以在头文件中定义一个枚举类型表明DCX的状态,例如:

typedef enum{isCommand,isData
}DCX;     /* 数据/命令切换控制状态 */

然后调用前面的WriteDCX函数以及SPI的write函数发送指令,代码如下:

static void OLEDDrvWriteReg(uint8_t ucData)
{OLEDDrvWriteDCX(isCommand);fsp_err_t err = g_spi0.p_api->write(g_spi0.p_ctrl, (uint8_t*)&ucData, 1, SPI_BIT_WIDTH_8_BITS);if(FSP_SUCCESS != err)printf("Function:%s\tLine:%d\r\n", __FUNCTION__, __LINE__);SPIWaitTxCplt();
}
  1. 发送数据函数

发送数据则需要将DCX拉高,然后再调用write函数。代码如下:

static void OLEDDrvWriteDat(uint8_t ucData)
{OLEDDrvWriteDCX(isData);fsp_err_t err = g_spi0.p_api->write(g_spi0.p_ctrl, (uint8_t*)&ucData, 1, SPI_BIT_WIDTH_8_BITS);if(FSP_SUCCESS != err)printf("Function:%s\tLine:%d\r\n", __FUNCTION__, __LINE__);SPIWaitTxCplt();
}
static void OLEDDrvWriteBuf(uint8_t* rbuf, unsigned short wSize)
{OLEDDrvWriteDCX(isData);fsp_err_t err = g_spi0.p_api->write(g_spi0.p_ctrl, (uint8_t*)rbuf, wSize, SPI_BIT_WIDTH_8_BITS);if(FSP_SUCCESS != err)printf("Function:%s\tLine:%d\r\n", __FUNCTION__, __LINE__);SPIWaitTxCplt();
}

要注意的是,第04行传输的是8位数据,它对应8个像素。

在某些显示设备的驱动芯片中,有可能是用16bit的来表示一个像素。

  1. SSD1306参数配置函数

SSD1306可以配置的参数有很多,比如对比度、功率、时钟等等,本书不会一一详细列举代码,仅以函数名称展示:

序号函数名参数作用
1void OLEDDrvSetContrastValue(uint8_t ucValue)ucValue:对比度值,范围0~255设置SSD1306的对比度
2void OLEDDrvEntrieDisp(bool bState)bState:true-局部显示;false-全屏显示;SSD1306全部显示或者局部显示:0xA5-局部显示;0xA4-全部显示
3void OLEDDrvSetDispMode(bool bMode)bMode:true-1显示0不显示;false-1不显示0显示;SSD1306正常显示还是颠倒显示
4void OLEDDrvSetHScroll(uint8_t ucDir, uint8_t ucStart, uint8_t ucFrtime, uint8_t ucEnd)ucDir:true-从左往右滚动;false—从右往左滚动设置横向滚动区域
5void OLEDDrvSetHVScrool(uint8_t ucDir, uint8_t ucStart, uint8_t ucFrtime, uint8_t ucEnd, uint8_t ucOffset)ucDir:true-从左往右滚动;false—从右往左滚动设置横向和纵向的滚动区域
6void OLEDDrvSetScroll(bool bState)bState:true-开启滚动;false-关闭滚动开启或者关闭滚动显示效果
7static void OLEDDrvSetScrollArea(uint8_t ucArea, uint8_t ucNumOfRow)ucArea-起始行;ucNumOfRow-滚动行数;设置滚动显示区域
8void OLEDDrvSetMemAddrMode(uint8_t ucMode)ucMode:1-页地址模式;2-横向地址模式;3-纵向地址模式设置地址模式
9void OLEDDrvSetColumnAddr(uint8_t ucStart, uint8_t ucEnd)ucStart-行起始地址;ucEnd-行结束地址在页地址模式下只有起始地址有效;
10void OLEDDrvSetPageAddr(uint8_t ucStart, uint8_t ucEnd)ucStart-页起始地址;ucEnd-页束地址在页地址模式下只有起始地址有效;
11void OLEDDrvDispON(struct DisplayDevice* ptDev)ptDev-指向显示设备开启显示功能
12void OLEDDrvDispOFF(struct DisplayDevice* ptDev)ptDev-指向显示设备关闭功能

其它硬件配置函数省略,使用默认值也可。
7. 设置显示区域函数

设置显示区域其实就是划定行地址和页地址的起始地址以及结束地址,将这两者封装到一起,将地址参数传入即可,参照如下代码:

static void OLEDDrvSetDispWindow(struct DisplayDevice* ptDev, unsigned short wXS, unsigned short wYS, unsigned short wXE, unsigned short wYE)
{if(NULL == ptDev->name)    return;OLEDDrvSetColumnAddr((uint8_t)wXS, (uint8_t)wXE);OLEDDrvSetPageAddr((uint8_t)wYS, (uint8_t)wYE);
}
  1. 绘制单个像素点函数

绘制单个像素首先需要知道绘制的地址是哪一个,假设将column地址设置为x,page地址设置为y,那么目标像素地址的换算公式如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

y除以8是因为共有8页,再乘以128是因为每一页有128个column。

另外,本章实验使用的是frame buffer,为此可以封装成以下函数:

static int OLEDDrvSetPixel(struct DisplayDevice* ptDev, unsigned short wX, unsigned short wY,unsigned short wColor)
{if(NULL == ptDev->name)    return -1;unsigned char *buf = ptDev->FBBase;int page;unsigned char *byte;int bit;if (wX >= ptDev->wXres || wY >= ptDev->wYres)return -1;page = wY / 8;byte = buf + page * 128 + wX;bit = wY % 8;if (wColor)*byte |= (1<<bit);else*byte &= ~(1<<bit);return 0;
}
  • 第07行:获取frame buffer的首地址;
  • 第13~14行:获取目标像素点在frame buffer中的偏移地址;
  • 第15行:获取像素点在横向地址的位置;因为修改的像素点位置大概率不会是页数的整数倍,而是某一页的某一位,因而需要计算出这个点在那一页的哪一位需要被修改。
  • 第16~19行:修改像素点的显示;
  1. 全屏刷新函数

对于SSD1306而言,刷新全屏就是发送128*8 bytes的数据到显存,因而调用前文介绍的一次性发送N字节的函数,代码如下:

static void OLEDDrvFulsh(struct DisplayDevice* ptDev)
{if(NULL == ptDev->name)    return;OLEDDrvWriteCS(isSelect);OLEDDrvWriteBuf(ptDev->FBBase, 128*8);OLEDDrvWriteCS(notSelect);
}

本章实验使用的是横向地址模式,因而可以一次性发送全部显存数据。

  1. 设备初始化函数

SSD1306的初始化,需要初始化涉及的 GPIO、初始化SPI控制器、初始化SSD1306本身,读者可以参考如下代码来初始化SSD1306:

static int OLEDDrvinit(struct DisplayDevice* ptDev)
{if(NULL == ptDev->name)    return -1;OLEDDrvGpioInit();OLEDDrvWriteCS(notSelect);OLEDDrvWriteDCX(isCommand);fsp_err_t err = g_spi0.p_api->open(g_spi0.p_ctrl, g_spi0.p_cfg);if(FSP_SUCCESS != err){printf("Error when open spi0 device!\r\n");return -1;}printf("Success to open device: spi0\r\n");OLEDDrvWriteCS(isSelect);OLEDDrvSetMemAddrMode(1);OLEDDrvSetMuxRatio(0x3F);OLEDDrvSetDispOffset(0x00);OLEDDrvSetDispStartLine(0);OLEDDrvSetSegRemap(true);OLEDDrvSetCOMScanDir(true);OLEDDrvSetCOMHWConfig(0, 0);OLEDDrvSetContrastValue(0x7F);OLEDDrvEntrieDisp(false);OLEDDrvSetDispMode(true);OLEDDrvSetDispClock(0x00, 0x08);OLEDDrvSetChargePump(true);OLEDDrvDispON(ptDev);OLEDDrvSetDispWindow(ptDev, 0, 0, ptDev->wXres-1, ptDev->wYres-1);OLEDDrvWriteCS(notSelect);return 0;
}

13.2.4 测试程序

本章的测试程序放在app_disp.c中实现,示例代码如下:

void DispAppTest(void)
{DisplayDevice *ptDispDev = OLEDGetDevice();if(ptDispDev == NULL){printf("Failed to get OLED Display Device!\r\n");return;}ptDispDev->Init(ptDispDev);uint8_t *pBuf = (uint8_t*)ptDispDev->FBBase;while(1){for(uint16_t i=0; i<128*8; i++){pBuf[i] = 0x55;}ptDispDev->Flush(ptDispDev);for(uint16_t i=0; i<128*8; i++){pBuf[i] = 0x00;}ptDispDev->Flush(ptDispDev);}
}

13.2.5 上机实验

在hal_entry.c中的hal_entry()函数中初始化串口设备以及调用显示设备的测试函数来使串口设备打印调试信息,让OLED开始测试显示,代码如下:

#include "app.h"
#include "drv_uart.h"
#include "hal_systick.h"
#include "hal_data.h"void hal_entry(void)
{/* TODO: add your own code here */SystickInit();UARTDrvInit();DispAppTest();
}

将程序编译出来的二进制可执行文件烧录到板子上,运行后可以看到OLED在不断地示横线、清屏。


本章完

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/75382.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

QT(9.4)tcp通信,数据库,opencv,

作业&#xff1a; 1.服务器 头文件&#xff1a; #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTcpSocket>//客户端头文件 #include <QMessageBox>//消息对话框头文件 #include <QTcpServer>//服务器头文件 #include <list>…

Linux修复损坏的文件系统

如何判断文件系统是否损坏 当文件系统受损时&#xff0c;将会出现一些明显的迹象。例如&#xff0c;文件或文件夹无法访问、文件大小异常、系统启动慢或无法启动等。此外&#xff0c;系统也可能发出一些错误信息&#xff0c;如"Input/output error"、"Filesyst…

Acwing算法心得——现代艺术(统计遍历)

大家好&#xff0c;我是晴天学长&#xff0c;先用两个一维数组维护数据&#xff0c;再统计遍历二维数组&#xff0c;需要的小伙伴请自取哦&#xff01;&#x1f4aa;&#x1f4aa;&#x1f4aa; 1 &#xff09;现代艺术 2) .算法思路 现代艺术 1.两个数组维护行和列 2.遍历数组…

Spring框架简介

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

简明SQL条件查询指南:掌握WHERE实现数据筛选

条件查询是用于从数据库中根据特定条件筛选数据行的一种方式&#xff0c;它避免了检索整个表中的数据。通常&#xff0c;使用 WHERE 子句来定义过滤条件&#xff0c;只有符合这些条件的数据行才会被返回。 SQL中的运算符有&#xff1a;、!、<、> 等&#xff0c;用于进行…

「网页开发|前端开发|Vue」06 公共组件与嵌套路由:让每一个页面都平等地拥有导航栏

本文主要介绍在多个页面存在相同部分时&#xff0c;如何提取公共组件然后在多个页面中导入组件重复使用来减少重复代码。在这基础上介绍了通过嵌套路由的方式来避免页面较多或公共部分较多的情况下&#xff0c;避免不断手动导入公共组件的麻烦&#xff0c;并且加快页面跳转的速…

KT142C-sop16语音芯片ic的串口指令详细说明_默认9600指令可设

3.1 通讯格式 支持异步串口通讯模式,通过串口接受上位机发送的命令 通讯标准:9600 bps --- 可以发送指令修改&#xff0c;并且记忆&#xff0c;详见3.4.5 数据位 :8 停止位 :1 校验位 :none 流控制 :none 格式&#xff1a;$S VER Len CMD Feedback para1 …

PIGOSS BSM:网络大屏展现功能与特色全面解析

导语 PIGOSS BSM是一款强大的IT运维监控工具&#xff0c;提供了丰富的功能和特色。其中的“网络大屏”模块是其核心功能之一&#xff0c;能够以直观、全面的方式展示网络设备的状态信息和各种关键指标。本文将详细介绍PIGOSS BSM网络大屏的功能及特色&#xff0c;让您全面了解其…

多线程与高并发——并发编程(6)

文章目录 六、并发集合1 ConcurrentHashMap1.1 存储结构1.2 存储操作1.2.1 put方法1.2.2 putVal方法-散列算法1.2.3 putVal方法-添加数据到数组&初始化数组1.2.4 putVal方法-添加数据到链表1.3 扩容操作1.3.1 treeifyBin方法触发扩容1.3.2 tryPresize方法-针对putAll的初始…

React16、18 使用 Redux

Redux 核心 Redux 介绍 Redux 是javaScript 状态容器&#xff0c;提供可预测化的状态管理 Redux 工作流程 Actions&#xff1a;对象&#xff0c;描述对状态进行怎样的操作 Reducer&#xff1a;函数&#xff0c;操作状态并返回新的状态 Store&#xff1a;存储状态的容器&am…

Unity Asset Bundle Browser 工具

Unity Asset Bundle Browser 工具 您可以在 Unity 项目中使用 Asset Bundle Browser 工具能够查看和编辑资源包的配置。 有关更多信息&#xff0c;请参阅 Unity Asset Bundle Browser 文档。 注意&#xff1a;此工具是不受支持的实用程序。查看极大的资源包可能会导致性能下…

关于µC/OS-III 多任务的基本理解

关于C/OS-III 多任务的基本理解 任务和任务管理是 RTOS 的核心&#xff0c;且大多数项目使用 RTOS 的目的就是为了使用 RTOS 的多任务管理能力。 C/OS-III作为经典的RTOS&#xff0c;了解并学习其任务管理机制&#xff0c;是非常有必要的。 文章目录 关于C/OS-III 多任务的基本…

SQL SERVER 如何实现UNDO REDO 和PostgreSQL 有近亲关系吗

开头还是介绍一下群&#xff0c;如果感兴趣PolarDB ,MongoDB ,MySQL ,PostgreSQL ,SQL Server&#xff0c;Redis &#xff0c;Oracle ,Oceanbase 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请加微信号 l…

Excel VSTO开发5 -Excel对象结构

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 5 Excel对象结构 Excel提供了几个比较重要的对象&#xff1a; Application、Workbooks、Workbook、Worksheets、Worksheet 为了便…

Ubantu终端常用命令、快捷键和基本操作

目录 前言 一、常用命令 二、常用快捷键 三、快捷键自定义设置 总结 前言 Ubantu终端常用命令和快捷键用于进行系统管理、文件操作、软件安装等常见使用场景。使用它们可以提高工作效率&#xff0c;简化操作流程&#xff0c;并进行更多的自定义配置和控制。同时&#xff0c…

jenkins创建用户

一.背景 之前用了很多次&#xff0c;现在转到甲方爸爸的岗位&#xff0c;要培养大学毕业生&#xff0c;才发现好记性不如烂笔头。给年轻人写出来。 二.创建用户的过程 1.用户管理界面入口 Dashboard>Manage Jenkins>Jenkins own user database 2.点击右边的按钮“Cre…

深入《C++ Core Guidelines解析》:提升C++编程实践的关键指南

目录 1、写在前面2、推荐理由3、内容介绍4、作者介绍5、赠书 or 购买 1、写在前面 C Core Guidelines是一个正在进行的开源项目&#xff0c;通过将广泛认可的现代C上佳实践集中在一个地方来解决这些问题。Core Guidelines依赖于几十年的经验和早期的编码规则。它们与C本身共享一…

数据结构 每日一练:将带头结点的单链表就地逆置(视频讲解两种方法)

目录 方法一 算法视频分析 方法二 算法视频分析 Q&#xff1a;什么是“就地”捏&#xff1f; A&#xff1a;就是指辅助空间复杂度为O(1)&#xff0c;通俗一点来说就是不需要再开辟一块空间来实现算法。 特别说明&#xff1a; 笔者第一次录制视频&#xff0c;言语有些不顺&…

MySQL——常见问题

NULL和空值的区别 1、空值不占空间&#xff0c;NULL值占空间。当字段不为NULL时&#xff0c;也可以插入空值。 2、当使用 IS NOT NULL 或者 IS NULL 时&#xff0c;只能查出字段中没有不为NULL的或者为 NULL 的&#xff0c;不能查出空值。 3、判断NULL 用IS NULL 或者 is no…

Win10 ping 虚拟机kali 请求超时解决办法

出现这种问题应该是windows休眠导致的 这里我的解决方法是先禁用再启用连接 然后再ping 虚拟机ip和kali ip&#xff0c;发现就可以连上了