【星闪】Hi2821 | SPI串行外设接口 + OLED显示屏驱动例程
1. 简介
SPI(Serial Peripheral interface)全称串行外围设备接口,是一种高速、全双工、同步通信总线。
SPI 采用主从控制模式(Master-Slave)架构,一般有一个主设备、一个或多个从设备,使得主设备可以与多个从设备之间实现片间通信。SPI 通信需要 4 根信号线——时钟线(SCLK)、主出从入线(MOSI)、主入从出线(MISO)和片选线(CS)。
每增加一个从机那么就要增加一条片选线,理论上片选线可以无限增加,如果总线上的外设通信效率不需要很高的话。
1.1 工作模式
SPI 的通讯方式比较简单,主机主动发起通讯,先拉低对应从机的片选线(CS)通知开始通讯,接着时钟线发送脉冲,在时钟脉冲的上升沿或下降沿,两者接收或者发送数据。
在 SPI 的通讯过程中,既可以上升沿采集数据,也可以下降沿采集数据,这取决于 SPI 的工作模式,工作模式由时钟极性(CPOL)和相位(CPHA)决定。
- CPOL=0:时钟空闲时为低电平;
- CPOL=1:时钟空闲时为高电平;
- CPHA=0:在第一个时钟边沿采集数据;
- CPHA=1:在第二个时钟边沿采集数据;
根据上面的描述可以知道,当 CPOL=0、CPHA=0 或 CPOL=1、CPHA=1 时,通信时在上升沿采集数据;当 CPOL=0、CPHA=1 或 CPOL=0、CPHA=1 时,通信时在下降沿采集数据。
2. 例程
这个例程中会使用 SPI 协议去驱动一块 OLED 显示屏,屏幕使用 SSD1306 驱动芯片,要注意只有 7 个引脚的版本才支持 SPI 协议通讯,如下图:
SSD1306 驱动芯片数据手册:下载链接
2.1 Kconfig
以下是比较常用的配置
- SPI support MASTER:使能主机功能;
- SPI support SLAVE:使能从机功能;
- SPI support DMA transfer:使能 DMA 传输;
- SPI support interrupt transfer:使能中断传输(不能与 DMA 传输同时打开);
- SPI support concurrency:使能并发传输;
另外在芯片配置这里可以调整 SPI 总线的数量,可以根据使用的外设数量来调整。
2.2 代码
#include <stdbool.h>#include "oled.h"#include "spi.h"
#include "pinctrl.h"
#include "gpio.h"
#include "dma.h"#define LOG_TAG "oled"
#include "debug.h"#define SPI_BUS SPI_BUS_1#define CS_PIN(x) uapi_gpio_set_val(11, x)
#define DC_PIN(x) uapi_gpio_set_val(12, x)typedef struct {uint8_t p1; // 起始页uint8_t p2; // 结尾页uint8_t y1_off; // y距页起始的偏移uint8_t y2_off; // y距页结尾的偏移
} window_t;static uint8_t g_gram[(CONFIG_SCREEN_HEIGHT >> 3) * (CONFIG_SCREEN_WIDTH)] = {0};static int oled_bsp_init(void)
{int ret = 0;/* SCLK */uapi_pin_set_mode(15, HAL_PIO_SPI1_CLK);/* MOSI */uapi_pin_set_mode(14, HAL_PIO_SPI1_TXD);/* RST */uapi_pin_set_mode(13, HAL_PIO_FUNC_GPIO);uapi_gpio_set_dir(13, GPIO_DIRECTION_OUTPUT);uapi_gpio_set_val(13, GPIO_LEVEL_HIGH);/* DC */uapi_pin_set_mode(12, HAL_PIO_FUNC_GPIO);uapi_gpio_set_dir(12, GPIO_DIRECTION_OUTPUT);/* CS */uapi_pin_set_mode(11, HAL_PIO_FUNC_GPIO);uapi_gpio_set_dir(11, GPIO_DIRECTION_OUTPUT);uapi_gpio_set_val(11, GPIO_LEVEL_HIGH);/* 初始化SPI */spi_attr_t config = { 0 };config.freq_mhz = 8; /* 工作频率 */config.is_slave = false; /* 主机模式 */config.frame_size = HAL_SPI_FRAME_SIZE_8; /* 帧大小,8位 */config.slave_num = 1; /* 片选 0 */config.spi_frame_format = HAL_SPI_FRAME_FORMAT_STANDARD; /* 传输模式:标准 */config.bus_clk = SPI_CLK_FREQ; /* 总线速率 32MHz */config.frame_format = SPI_CFG_FRAME_FORMAT_MOTOROLA_SPI; /* 协议格式:摩托罗拉SPI协议格式 */config.tmod = HAL_SPI_TRANS_MODE_TXRX; /* 传输模式:收发模式 */config.clk_phase = SPI_CFG_CLK_CPHA_0; /* 相位:空闲低电平 */config.clk_polarity = SPI_CFG_CLK_CPOL_0; /* 极性:第一个时钟沿采集 */spi_extra_attr_t ext_config = { 0 };ext_config.sspi_param.wait_cycles = 0x10; /* 等待周期 */ret = uapi_spi_init(SPI_BUS, &config, &ext_config);if (ret) {return ret;}return 0;
}static int oled_write_bytes(bool is_data, uint8_t* bytes, size_t len)
{spi_xfer_data_t data = {.tx_buff = bytes,.tx_bytes = len,};DC_PIN(is_data);CS_PIN(0);int ret = uapi_spi_master_write(SPI_BUS, &data, CONFIG_SPI_MAX_TIMEOUT);CS_PIN(1);return ret;
}static int oled_write_cmd(uint8_t cmd)
{return oled_write_bytes(false, &cmd, 1);
}static void oled_reg_init(void)
{/* 进入睡眠 */oled_write_cmd(0xAE);/* 起始行地址 */oled_write_cmd(0x40);/* 对比度 */oled_write_cmd(0x81);oled_write_cmd(0xCF); // A[7:0]/* 段重映射 */oled_write_cmd(0xA1); // 0xA0:左右反置,0xA1:正常/* COM输出扫描方向 */oled_write_cmd(0xC8); // 0xC0:上下反置,0xC8:正常/* Multiplex Ratio */oled_write_cmd(0xA8);oled_write_cmd(0x3f); // 1/64 duty/* 屏幕偏移 */oled_write_cmd(0xD3);oled_write_cmd(0x00); // A[5:0]/* 时钟频率 */oled_write_cmd(0xD5);oled_write_cmd(0x80); // A[3:0]+1:分频系数;A[7:4]:晶振频率/* 预充电周期 */oled_write_cmd(0xD9);oled_write_cmd(0xF1); // A[3:0]:第一阶段;A[7:4]:第二阶段/* COM配置 */oled_write_cmd(0xDA);oled_write_cmd(0x12); // A[4]=0:序列;A[4]=1:另类;A[5]:COM左右重映射/* Vcomh */oled_write_cmd(0xDB);oled_write_cmd(0x40); // A[6:4]/* 内存取址模式 */oled_write_cmd(0x20);oled_write_cmd(0x00); // A[1:0]:0x00:行取址;0x01:列取址;0x02:页取址oled_write_cmd(0x8D);//--set Charge Pump enable/disableoled_write_cmd(0x14);//--set(0x10) disable/* 屏幕使能 */oled_write_cmd(0xA4); // 0xA4:使用RAM内容;0xA5:忽略RAM内容/* 屏幕反转 */oled_write_cmd(0xA6); // 0xA6:正常;0xA7:反转/* 唤醒屏幕 */oled_write_cmd(0xAF);/* 清屏 */oled_clear();
}static int oled_open_window(uint8_t x, uint8_t y, uint8_t w, uint8_t h, window_t* window)
{if (window == NULL ||x >= CONFIG_SCREEN_WIDTH ||y >= CONFIG_SCREEN_HEIGHT ||w + x > CONFIG_SCREEN_WIDTH ||h + y > CONFIG_SCREEN_HEIGHT ||w == 0 || h == 0) {LOG("invalid param");return -1;}window->y1_off = y % 8;window->y2_off = (y + h + window->y1_off) % 8 ? 8 - ((y + h + window->y1_off) % 8) : 0;window->p1 = y >> 3;window->p2 = ((y + h + window->y1_off + window->y2_off) >> 3) - 1;/* 设置列区间 */oled_write_cmd(0x21);oled_write_cmd(x);oled_write_cmd(x + w - 1);/* 设置页区间 */oled_write_cmd(0x22);oled_write_cmd(window->p1);oled_write_cmd(window->p2);return 0;
}int oled_init(void)
{int ret = 0;/* 初始化板级驱动 */ret = oled_bsp_init();if (ret) {LOG("oled bsp init failed, err: %d", ret);return ret;}/* 初始化寄存器 */oled_reg_init();return 0;
}int oled_set_data(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t* data)
{int ret = 0;/* 开窗 */window_t window = {0};ret = oled_open_window(x, y, w, h, &window);if (ret) {LOG("open window failed");return ret;}// LOG("p1=%d, p2=%d, y1_off=%d, y2_off=%d", window.p1, window.p2, window.y1_off, window.y2_off);/* 缓存数据 */for (uint8_t i = window.p1, *ptr = data; i <= window.p2; i++, ptr += w) {memcpy(g_gram + i * CONFIG_SCREEN_WIDTH + x, ptr, w);}/* 写数据 */return oled_write_bytes(true, g_gram + window.p1 * CONFIG_SCREEN_WIDTH + x, (window.p2 - window.p1 + 1) * w);
}void oled_clear(void)
{memset(g_gram, 0, sizeof(g_gram));/* 设置列区间 */oled_write_cmd(0x21);oled_write_cmd(0);oled_write_cmd(CONFIG_SCREEN_WIDTH - 1);/* 设置页区间 */oled_write_cmd(0x22);oled_write_cmd(0);oled_write_cmd((CONFIG_SCREEN_HEIGHT >> 3) - 1);/* 写数据 */oled_write_bytes(true, g_gram, sizeof(g_gram));
}
1. 初始化管脚。
SPI 的相关管脚需要调用 uapi_pin_set_mode 来配置复用,但片选线比较特殊,像我这块 OLED 屏幕的显示芯片使用的是 3 线 SPI,即没有 MISO 线,那么片选线就不需要复用,配置成普通 GPIO 即可;如果使用的是标准 SPI,那么片选线就要配置复用。
除了 SPI 相关的管脚还有一根 DC 线用于控制命令发送或数据发送和一根 RES 线用于复位芯片(不需要可以直接接3.3V电源)。
2. 初始化 SPI 接口。
调用 uapi_spi_init 函数进行初始化,参数一为总线号;参数二为初始化结构体,定义如下:
typedef struct hal_spi_attr {bool is_slave; /*!< @if Eng Indicates if SPI work in slave mode or not.@else SPI工作在Master/Slave模式。 @endif */uint32_t slave_num; /*!< @if Eng Index when selecting a slave.- 0: Not select.- 1: slave index 0.- 2: slave index 1.- ...@else 选择从机时的索引- 0:不选择。- 1:从机索引0。- 2:从机索引1。- ...@endif */uint32_t bus_clk; /*!< @if Eng Provide ssi_clk for clock freq division calculation.@else 用于计算SPI的时钟分频系数。 @endif */uint32_t freq_mhz; /*!< @if Eng Indicates the frequency of SPI.@else SPI的工作频率。 @endif */uint32_t clk_polarity; /*!< @if Eng Indicates the clock polarity of SPI.For details, see @ref hal_spi_cfg_clk_cpol_t@else SPI的时钟极性。参考 @ref hal_spi_cfg_clk_cpol_t @endif */uint32_t clk_phase; /*!< @if Eng Indicates the clock phase of SPI.For details, see @ref hal_spi_cfg_clk_cpha_t@else SPI的时钟相位。参考 @ref hal_spi_cfg_clk_cpha_t @endif */uint32_t frame_format; /*!< @if Eng Indicates the which serial protocol transfers the data.For details, see @ref hal_spi_cfg_frame_format_t@else 选择串行传输的协议。参考 @ref hal_spi_cfg_frame_format_t @endif */uint32_t spi_frame_format; /*!< @if Eng Indicates the frame format of SPI.For details, see @ref hal_spi_frame_format_t@else SPI的帧格式。参考 @ref hal_spi_frame_format_t @endif */uint32_t frame_size; /*!< @if Eng Indicates the frame size of SPI.For details, see @ref hal_spi_frame_size_t@else SPI的帧长度。参考 @ref hal_spi_frame_size_t @endif */uint32_t tmod; /*!< @if Eng Indicates the transfer mode.For details, see @ref hal_spi_trans_mode_t@else SPI的传输模式。参考 @ref hal_spi_trans_mode_t @endif */uint32_t ndf; /*!< @if Eng Indicates the number of data frames.@else SPI的数据帧数。 @endif */uint32_t sste; /*!< @if Eng Indicates if SPI slave select toggle enable or not.When disable, master should reed all data in slave tx_queueat ONE time when reading data from slave device. Otherwise,data loss occurs.For details, see @ref hal_spi_cfg_sste_t@else SPI从机选择切换使能/不使能。当此配置不使能,主机从从机读取数据时,需要一次性将从机发送队列中的数据读完,否则会出现丢失数据问题。参考 @ref hal_spi_cfg_sste_t @endif */
} hal_spi_attr_t;
- is_slave:是否为从机;
- slave_num:从机数量;
- bus_clk:总线时钟频率,单位:Hz,SPI 默认使用 32 MHz 时钟。
- freq_mhz:工作时钟频率,单位:MHz;
工作时钟频率可以通过数据手册查看,
是最小的时钟周期,为100ns,因此可以计算出最高时钟频率为 10MHz。
- clk_polarity:时钟极性(CPOL);
- clk_phase:时钟相位(CPHA);
根据时序图,芯片支持 CPOL=0、CPHA=0 和 CPOL=1、CPHA=1,两种工作方式。
- frame_format:帧传输协议,选项如下:
typedef enum hal_spi_cfg_frame_format {SPI_CFG_FRAME_FORMAT_MOTOROLA_SPI, /*!< @if Eng Motorolla SPI Frame Format.@else 摩托罗拉SPI帧格式。 @endif */SPI_CFG_FRAME_FORMAT_TEXAS_SSP, /*!< @if Eng Texas Instruments SSP Frame Format.@else 德州仪器SSP帧格式。 @endif */SPI_CFG_FRAME_FORMAT_NS_MICROWIRE, /*!< @if Eng National Microwire Frame Format.@else 国家微线帧格式。 @endif */SPI_CFG_FRAME_FORMAT_MAX
} hal_spi_cfg_frame_format_t;
- spi_frame_format:帧格式,选项如下:
typedef enum hal_spi_frame_format {HAL_SPI_FRAME_FORMAT_STANDARD = 0, /*!< @if Eng SPI Standard frame format.@else 标准的单线SPI帧格式。 @endif */HAL_SPI_FRAME_FORMAT_DUAL, /*!< @if Eng SPI Dual frame format.@else 双线SPI帧格式。 @endif */HAL_SPI_FRAME_FORMAT_QUAD, /*!< @if Eng SPI Quad frame format.@else 4线SPI帧格式。 @endif */HAL_SPI_FRAME_FORMAT_OCTAL, /*!< @if Eng SPI Octal frame format.@else 8线SPI帧格式。 @endif */HAL_SPI_FRAME_FORMAT_DOUBLE_OCTAL, /*!< @if Eng SPI Double Octal frame format.@else 16线SPI帧格式。 @endif */HAL_SPI_FRAME_FORMAT_SIXT,HAL_SPI_FRAME_FORMAT_MAX_NUM,HAL_SPI_FRAME_FORMAT_NONE = HAL_SPI_FRAME_FORMAT_MAX_NUM
} hal_spi_frame_format_t;
- frame_size:数据帧大小,选项如下:
typedef enum hal_spi_frame_size {HAL_SPI_FRAME_SIZE_8 = 0x07, /*!< @if Eng 8-bit serial data transfer.@else 8-位串行数据传输。 @endif */HAL_SPI_FRAME_SIZE_16 = 0x0F, /*!< @if Eng 16-bit serial data transfer(Not supported now).@else 16-位串行数据传输(暂不支持)。 @endif */HAL_SPI_FRAME_SIZE_24 = 0x17, /*!< @if Eng 24-bit serial data transfer(Not supported now).@else 24-位串行数据传输(暂不支持)。 @endif */HAL_SPI_FRAME_SIZE_32 = 0x1F /*!< @if Eng 32-bit serial data transfer.@else 32-位串行数据传输。 @endif */
} hal_spi_frame_size_t;
- tmod:传输模式,选项如下:
typedef enum hal_spi_trans_mode {HAL_SPI_TRANS_MODE_TXRX = 0, /*!< @if Eng Transmit and receive mode.@else 收发模式。 @endif */HAL_SPI_TRANS_MODE_TX, /*!< @if Eng Transmit only / Transmit mode.@else 发送模式。 @endif */HAL_SPI_TRANS_MODE_RX, /*!< @if Eng Receive only / Receive mode.@else 接收模式。 @endif */HAL_SPI_TRANS_MODE_EEPROM, /*!< @if Eng EEPROM read mode.@else EEPROM模式。 @endif */HAL_SPI_TRANS_MODE_MAX
} hal_spi_trans_mode_t;
- ndf:数据帧数量;
- sste:从机片选切换使能,即软件片选或硬件片选。
参数三为拓展的 SPI 配置参数,主要用于一些特殊的模式,如 QSPI、EEPROM、DMA 等等,初始化结构体定义如下:
typedef struct hal_spi_extra_attr {bool tx_use_dma; /*!< @if Eng Indicates if SPI use dma or not in TX.@else SPI是否使用DMA发送数据。 @endif */bool rx_use_dma; /*!< @if Eng Indicates if SPI use dma or not in RX.@else SPI是否使用DMA接收数据。 @endif */hal_spi_xfer_qspi_param_t qspi_param; /*!< @if Eng Indicates the qspi parameters.@else QSPI参数。 @endif */hal_spi_xfer_sspi_param_t sspi_param; /*!< @if Eng Indicates the single spi parameters.@else Single SPI参数。 @endif */
} hal_spi_extra_attr_t;typedef struct hal_spi_xfer_qspi_param {hal_spi_trans_type_t trans_type; /*!< @if Eng SPI frame format for instruction and address.@else 传输类型,用于指定指令和地址的长度。 @endif */hal_spi_inst_len_t inst_len; /*!< @if Eng Instruction length, support 0, 4, 8, 16bits.@else 指令长度,支持0、4、8、16位。 @endif */hal_spi_addr_len_t addr_len; /*!< @if Eng Address length, support 0, 8, 16, 24, 32bits.@else 地址长度,支持0、8、16、24、32位。 @endif */uint32_t wait_cycles; /*!< @if Eng Indicates the wait cycles.@else 等待的周期数。 @endif */
} hal_spi_xfer_qspi_param_t;typedef struct hal_spi_xfer_sspi_param {uint32_t wait_cycles; /*!< @if Eng Indicates the wait cycles.@else 等待的周期数。 @endif */
} hal_spi_xfer_sspi_param_t;
3. 初始化寄存器。
驱动芯片的寄存器初始化可以参考数据手册说明。
4. 写数据。
参考时序图,通讯前先拉低片选;接着,如果发送数据就拉高 DC 脚,发送命令就拉低 DC 脚;然后就可以调用 uapi_spi_master_write 函数发送数据,参数一为总线号,参数二为数据包,定义如下:
typedef struct hal_spi_xfer_data {uint8_t *tx_buff; /*!< @if Eng Buff to send data through tx fifo.@else 通过tx fifo发送数据的Buff。 @endif */uint32_t tx_bytes; /*!< @if Eng Bytes of data need to send. For details, see @ref hal_spi_attr_t.frame_size.when frame_size is HAL_SPI_FRAME_SIZE_8, The value must be a multiple of 1.when frame_size is HAL_SPI_FRAME_SIZE_16, The value must be a multiple of 2.when frame_size is HAL_SPI_FRAME_SIZE_24, The value must be a multiple of 3.when frame_size is HAL_SPI_FRAME_SIZE_32, The value must be a multiple of 4.@else 发送数据的个数。参考 @ref hal_spi_attr_t.frame_size.如果frame_size为HAL_SPI_FRAME_SIZE_8,则需设定为1的倍数如果frame_size为HAL_SPI_FRAME_SIZE_16,则需设定为2的倍数如果frame_size为HAL_SPI_FRAME_SIZE_24,则需设定为3的倍数如果frame_size为HAL_SPI_FRAME_SIZE_32,则需设定为4的倍数@endif */uint8_t *rx_buff; /*!< @if Eng Buff to receive data from rx fifo.@else 通过rx fifo接收数据的Buff。 @endif */uint32_t rx_bytes; /*!< @if Eng Bytes of data need to receive, For details, see @ref hal_spi_attr_t.frame_size.when frame_size is HAL_SPI_FRAME_SIZE_8, The value must be a multiple of 1.when frame_size is HAL_SPI_FRAME_SIZE_16, The value must be a multiple of 2.when frame_size is HAL_SPI_FRAME_SIZE_24, The value must be a multiple of 3.when frame_size is HAL_SPI_FRAME_SIZE_32, The value must be a multiple of 4.@else 接收数据的个数。参考 @ref hal_spi_attr_t.frame_size.如果frame_size为HAL_SPI_FRAME_SIZE_8,则需设定为1的倍数如果frame_size为HAL_SPI_FRAME_SIZE_16,则需设定为2的倍数如果frame_size为HAL_SPI_FRAME_SIZE_24,则需设定为3的倍数如果frame_size为HAL_SPI_FRAME_SIZE_32,则需设定为4的倍数@endif */uint8_t cmd; /*!< @if Eng Command for QSPI mode.@else QSPI模式下的命令。 @endif */uint8_t reserved[3]; /*!< @if Eng Reserved.@else 保留。 @endif */uint32_t addr; /*!< @if Eng Address for QSPI mode.@else QSPI模式下的地址。 @endif */
} hal_spi_xfer_data_t;
- tx_buff:发送缓冲区;
- tx_bytes:发送字节数;
- rx_buff:接收缓冲区;
- rx_bytes:接收字节数;
- cmd:命令(仅 QSPI);
- addr:地址(仅 QSPI)。
参数三为超时等待时间,单位:毫秒;传输完成后需要拉高片选,示意传输结束。
SSD1306 芯片有 3 种写显存方式——页优先、行优先和列优先,例程中使用的是行优先,示意图如下:
写显存时要先通过命令确定起始列、结束列、起始页和结束页。然后发送显示数据,芯片会按行依次填充数据,当发送完最后一个数据,指针会重新跳回起始位置。
行方向上的数据是通过页来储存的,相当于用一个字节来储存同一列的 8 行数据,所以写数据的时候要做一些额外的操作来实现非 8 比特对齐的写显存。
5. 主函数。
#include <string.h>#include "soc_osal.h"
#include "app_os_init.h"
#include "common_def.h"#include "oled.h"static uint8_t data[2 * 128];void app_main(void *unused)
{(void)(unused);/* 初始化OLED */oled_init();memset(data, 0xFF, sizeof(data));oled_set_data(0, 0, 128, 16, data);while (1) {osal_msleep(10);}
}
主函数里面先初始化 OLED,然后填充一块区域,测试工作是否正常。