飛書文檔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通信,以及計算機與嵌入式設備間的通信。 | I2C 因其簡潔的連線和地址機制,適用于板級設備間的通信,如傳感器、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)定義了時鐘空閑狀態電平: |
不開啟CRC檢驗
NSS為軟件控制
我們用得更多的是由軟件控制某些 GPIO引腳單獨作為SS信號,這個GPIO引腳可以隨便選擇,像板卡中只有一個從設備,我們可以不使用片選引腳。如果我們把 NSS引腳配置為硬件自動控制,SPI模塊能夠自動判別它能否成為SPI的主機,或自動進入SPI從機模式。
生成代碼
輪詢:最基本的發送接收函數,就是正常的發送數據和接收數據 |
中斷方式
中斷模式和串口比較類似,但是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接收模式: |
DMA使用注意事項
- 1、要確保從機啟動完成后,才開啟主機DMA發送或查詢,否則會出現丟幀
- 2、如果手動開關片選,要在DMA發送完成后才切換,否則會出現丟幀
- 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部分分配如下: |
查看原理圖
可以知道驅動屏幕的主要是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、OLED_RES
最終添加USART1作為日志串口,方便調試
初始化屏幕
然后設置對應寄存器,這部分一般參考廠家的示例或手冊。
/* USER CODE BEGIN 0 */
static void sh1106_reset()
{//復位屏幕HAL_GPIO_WritePin(GPIOB, OLED_RES_Pin, GPIO_PIN_RESET);? //RES resetHAL_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 panelsh1106_write_cmd(0x00);//---set low column address 00->02sh1106_write_cmd(0x10);//---set high column addresssh1106_write_cmd(0x40);//--set start line address? Set Mapping RAM Display Start Line (0x00~0x3F)sh1106_write_cmd(0x81);//--set contrast control registersh1106_write_cmd(0xCF);// Set SEG Output Current Brightnesssh1106_write_cmd(0xA1);//--Set SEG/Column Mapping????sh1106_write_cmd(0xC0);//Set COM/Row Scan Direction??sh1106_write_cmd(0xA6);//--set normal displaysh1106_write_cmd(0xA8);//--set multiplex ratio(1 to 64)sh1106_write_cmd(0x3f);//--1/64 dutysh1106_write_cmd(0xD3);//-set display offset Shift Mapping RAM Counter (0x00~0x3F)sh1106_write_cmd(0x00);//-not offsetsh1106_write_cmd(0xd5);//--set display clock divide ratio/oscillator frequencysh1106_write_cmd(0x80);//--set divide ratio, Set Clock as 100 Frames/Secsh1106_write_cmd(0xD9);//--set pre-charge periodsh1106_write_cmd(0xF1);//Set Pre-Charge as 15 Clocks & Discharge as 1 Clocksh1106_write_cmd(0xDA);//--set com pins hardware configurationsh1106_write_cmd(0x12);sh1106_write_cmd(0xDB);//--set vcomhsh1106_write_cmd(0x40);//Set VCOM Deselect Levelsh1106_write_cmd(0x20);//-Set Page Addressing Mode (0x00/0x01/0x02)sh1106_write_cmd(0x02);//sh1106_write_cmd(0x8D);//--set Charge Pump enable/disablesh1106_write_cmd(0x14);//--set(0x10) disablesh1106_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 |
設置列起始行地址
列地址由兩個字節分別管理高低四位。D7 D6 D5 D4是固定的
發送起始列地址:發送指令0x00-0x0F,0x10-0x1F,下方是轉換方法:
列地址由兩個字節分別管理高低四位。 |
從設定的頁和列開始發送數據,列地址自動累加,頁地址不會更新,如果超出范圍則超出部分無效。
/* 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; //填充0sh1106_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; //填充0sh1106_write_data(Buffer2[j]); //發送數據,每次發送1byte,8bit}xxx
}//優化后uint8_t s_chDispalyBuffer[128][8]; //8*8bit=64void sh1106_clear_screen()?
{uint8_t i, j;for (i = 0; i < 8; i ++) {sh1106_write_cmd(0xB0 + i); //設置頁碼從0xB0開始到0xB7sh1106_write_cmd(0x02); //列起始地址低四位sh1106_write_cmd(0x10); //列起始地址高四位//8*128個點,全部清零for (j = 0; j < 128; j ++) {s_chDispalyBuffer[j][i] = 0; //填充0sh1106_write_data(s_chDispalyBuffer[j][i]); //發送數據}}
}
畫點函數
/* USER CODE BEGIN 0 */
/**把需要點亮的點轉換為顯示數組s_chDispalyBuffer的一個bit狀態chXpos: 繪制點的x坐標 0<= x <=127chYpos: 繪制點的y坐標 0<= 7 <=63chPoint: 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); //把對應位置1if (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 */