從學生時代結束進入職場後,被分配做了嵌入式開發,以前的軟件知識能夠用到的地方較爲侷限。領導讓我學習的第一塊板子就是STM32F1系列,在跟着“正點原子”的教程學習了近一個月後,學習進入瓶頸期,這個階段並不知道應該再繼續看些什麼,由於有了例程代碼,所以自己的動手能力也比較侷限。
在同領導討論後,他讓我嘗試把幾個功能融合在一起玩一些實驗。只見他從架子上隨手拿了一塊小板子,說,來就這個吧,你試試用SPI驅動這塊板子,點亮數碼管。於是我迎來了我入職後完全自己動手的第二個實驗,有點忐忑。
在我嘗試幾天,並在前輩的悉心指導下,終於點亮了數碼管。那一瞬間真的是想哭,感覺數碼管上的光是我見過最美的光了=_+,又努力的調代碼,終於完全弄懂了驅動過程並正確的點亮了數碼管。
話不多說,下面開始總結實驗過程。
一、實驗環境
1. 硬件環境:STM32F103ZE開發板,TM1629A開發板(上面安置的是米字形數碼管)
2. 軟件環境:Keil 5
二、實驗思路
1. 所謂SPI驅動數碼管點亮,大家不要被驅動這個詞所嚇到。由於TM1629A芯片只能作爲從設備,只有數據接收口,沒有數據發送口,因此STM32作爲主設備,利用SPI通訊,將時鐘、數據、片選信息發送給TM1629A,併爲之供電,其實就是一種驅動,通過TM1629外部的作用,讓其內部運作起來;
2. SPI的配置要精確,它是整個實驗的基礎,而數碼管段位的點亮代碼,怎樣顯示1234ABCD這些,就比較簡單了;
3. 學習嵌入式,首先要學會看原理圖,瞭解引腳的作用,引腳同哪些硬件已經相連了,某些模塊的某個引腳配置了邏輯電源、邏輯地、上拉電阻等等。
比如“正點原子”的SPI通訊實驗,是STM32同板子上自帶的FLASH模塊W25QXX進行讀寫通訊,通過原理圖可以看到,SPI2的四條線(片選、時鐘、輸入、輸出)已經同這個模塊相連,因此教程中直接使用硬件SPI2與之通訊,也不需要再插線,那麼你想用硬件SPI1時,在配置完畢SPI1的寄存器,註釋掉例程中一開始的SPI2的相關代碼後,還需要用杜邦線將SPI1的四個引腳連到對應的SPI2四個引腳上,纔可以正常讀寫FLASH,因爲只有SPI2是和FLASH硬件上相連的(圖1)。
之後比較重要的是要學會看芯片的文檔,裏面會詳細介紹每個寄存器的用法與配置方法,引腳的作用,時序等等;
4. 第3點中提到的時序尤爲重要,下面用到時會詳細說明。
三、代碼說明
注:基礎配置,如sys.h等文件,請自行查閱“正點原子”相關教程,這裏不再贅述。
1. SPI的配置
實驗中我們選用STM32的硬件SPI1,根據原理圖(圖2),可以看到SPI1的片選、時鐘、數據輸入、數據輸出分別對應PA4、PA5、PA6、PA7.
從圖3,圖4中可以看出TM1629A只能作爲從設備,因此它的DIO數據輸入口應該用杜邦線連接到STM32F1的PA7數據輸出口,而PA6數據輸入則不用。故最後我們只需要PA4,PA5,PA7三個SPI口與TM1629A相連,同時STM32F1的5V供電與GND引腳,分別與TM1629A的10引腳、6引腳相連。
注:TM1629A的數據手冊中明確說明,應當使用5V供電。
我這邊爲了圖省事(沒找到排線),直接將TM1629的6、7、8、9、10五個腳焊上公對母杜邦線的公端,母端插上STM32F1的對應口(參見圖5)
① spi.h
#ifndef __SPI_H
#define __SPI_H
#include "sys.h"
void SPI_Init(void); //SPI初始化
u8 SPI1_ReadWriteByte(u8 TxData); //SPI讀寫一個字節
#define STB PAout(4) //片選引腳定義
#endif
這部分定義了SPI初始化函數以及讀寫函數。網上STM32F1驅動TM1629A的教程幾乎沒有,唯一相關的一份教程是將讀寫分開成兩個函數,但是我個人覺得SPI是以交換的形式讀寫數據,每發送一個數據必接收一個數據,每接收一個數據必發送一個數據,因此讀寫寫在一起便於理解一些。比如我們需要發送一個數據,而不在意從從設備讀取什麼信息,那直接忽略掉讀取的數據就好,我們讀取從設備一個數據,可以發0xff或者隨意一個數據給從設備進行交換就好。
② spi.c
#include "sys.h"
#include "spi.h"
void SPI_Init(void)
{
RCC->APB2ENR |= 1 << 2; //PORTA時鐘使能
RCC->APB2ENR |= 1 << 12; //SPI1時鐘使能
GPIOA->CRL &= 0x0000FFFF;
GPIOA->CRL |= 0xA0A20000; //PA5/7複用(TM1629A不支持高速傳輸,所以片選2MHz輸出就夠了)
GPIOA->ODR |= 0xB << 4; //PA5/7上拉
SPI1->CR1 |= 0 << 10; //(√)全雙工模式
SPI1->CR1 |= 1 << 9; //(√)軟件nss管理
SPI1->CR1 |= 1 << 8;
SPI1->CR1 |= 1 << 2; //(√)SPI主機
SPI1->CR1 |= 0 << 11; //(√)8bit數據格式
SPI1->CR1 |= 0 << 1; //(√)空閒模式下SCK爲0 CPOL=0
SPI1->CR1 |= 0 << 0; //(√)數據採樣從第一個時間邊沿開始,CPHA=0
SPI1->CR1 |= 2 << 3; //(√)Fsck=Fpclk1/8
SPI1->CR1 |= 1 << 7; //(√)LSBfirst
SPI1->CR1 |= 1 << 6; //SPI設備使能
SPI1_ReadWriteByte(0xff); //啓動傳輸
}
//以下讀寫代碼直接使用了“正點原子”的例程代碼
u8 SPI1_ReadWriteByte(u8 TxData)
{
u16 retry=0;
while((SPI1->SR & 1 << 1) == 0) //等待發送區空
{
retry++;
if(retry >= 0XFFFE)
return 0; //超時退出
}
SPI1->DR = TxData; //發送一個byte
retry = 0;
while((SPI1->SR & 1 << 0) == 0) //等待接收完一個byte
{
retry++;
if(retry >= 0XFFFE)
return 0; //超時退出
}
return SPI1->DR; //返回收到的數據
}
spi.c中包含的是SPI通訊驅動代碼。這裏需要注意的是,要仔細閱讀TM1629A的使用手冊,得知其通信的時鐘極性和相位,配置STM32F1的SPI時鐘極性和相位與之一致。TM1629A在空閒時爲低電平,第一個時鐘沿採樣,因此時鐘極性和相位都設置爲0。
2. 數碼管配置
③ seg.h
//外圈段碼代碼
#define SEG14_d 0x01
#define SEG14_e 0x02
#define SEG14_f 0x04
#define SEG14_a 0x08
#define SEG14_b 0x10
#define SEG14_c 0x20
#define SEG14_g2 0x40
#define SEG14_g1 0x80
//內圈段碼代碼
#define SEG14_n 0x01
#define SEG14_m 0x02
#define SEG14_l 0x04
#define SEG14_h 0x08
#define SEG14_j 0x10
#define SEG14_k 0x20
#define SEG14_colon 0x40
#define SEG14_empty 0x80
//零代碼
#define SEG14_zero 0x00
//字母編碼
#define CHAR_A_EXT (SEG14_b | SEG14_c | SEG14_g2)
#define CHAR_A_INT (SEG14_k | SEG14_l)
#define CHAR_B_EXT (SEG14_d | SEG14_e | SEG14_f | SEG14_g1 | SEG14_g2 | SEG14_c)
#define CHAR_B_INT (SEG14_zero)
#define CHAR_C_EXT (SEG14_a | SEG14_d | SEG14_e | SEG14_f)
#define CHAR_C_INT (SEG14_zero)
#define CHAR_D_EXT (SEG14_g1 | SEG14_g2 | SEG14_b | SEG14_c | SEG14_d | SEG14_e)
#define CHAR_D_INT (SEG14_zero)
#define CHAR_E_EXT (SEG14_a | SEG14_f | SEG14_g1 | SEG14_g2 | SEG14_e | SEG14_d)
#define CHAR_E_INT (SEG14_zero)
#define CHAR_F_EXT (SEG14_a | SEG14_f | SEG14_g1 | SEG14_g2 | SEG14_e)
#define CHAR_F_INT (SEG14_zero)
#define CHAR_G_EXT (SEG14_a | SEG14_b | SEG14_c | SEG14_d | SEG14_f | SEG14_g1 | SEG14_g2)
#define CHAR_G_INT (SEG14_zero)
#define CHAR_H_EXT (SEG14_b | SEG14_c | SEG14_e | SEG14_f | SEG14_g1 | SEG14_g2)
#define CHAR_H_INT (SEG14_zero)
#define CHAR_I_EXT (SEG14_a | SEG14_d)
#define CHAR_I_INT (SEG14_j | SEG14_m)
#define CHAR_J_EXT (SEG14_b | SEG14_c | SEG14_d | SEG14_e)
#define CHAR_J_INT (SEG14_zero)
#define CHAR_K_EXT (SEG14_e | SEG14_f | SEG14_g1)
#define CHAR_K_INT (SEG14_k | SEG14_n)
#define CHAR_L_EXT (SEG14_f | SEG14_e | SEG14_d)
#define CHAR_L_INT (SEG14_zero)
#define CHAR_M_EXT (SEG14_b | SEG14_c | SEG14_e | SEG14_f)
#define CHAR_M_INT (SEG14_h | SEG14_k | SEG14_m)
#define CHAR_N_EXT (SEG14_b | SEG14_c | SEG14_e | SEG14_f)
#define CHAR_N_INT (SEG14_h | SEG14_n)
#define CHAR_O_EXT (SEG14_c | SEG14_d | SEG14_e | SEG14_g1 | SEG14_g2)
#define CHAR_O_INT (SEG14_zero)
#define CHAR_P_EXT (SEG14_a | SEG14_b | SEG14_g1 | SEG14_g2 | SEG14_e | SEG14_f)
#define CHAR_P_INT (SEG14_zero)
#define CHAR_Q_EXT (SEG14_a | SEG14_b | SEG14_g1 | SEG14_g2 | SEG14_c | SEG14_f)
#define CHAR_Q_INT (SEG14_zero)
#define CHAR_R_EXT (SEG14_a | SEG14_b | SEG14_g1 | SEG14_g2 | SEG14_e | SEG14_f)
#define CHAR_R_INT (SEG14_n)
#define CHAR_S_EXT (SEG14_a | SEG14_f | SEG14_g1 | SEG14_g2 | SEG14_c | SEG14_d)
#define CHAR_S_INT (SEG14_zero)
#define CHAR_T_EXT (SEG14_a)
#define CHAR_T_INT (SEG14_j | SEG14_m)
#define CHAR_U_EXT (SEG14_b | SEG14_c | SEG14_d | SEG14_e | SEG14_f)
#define CHAR_U_INT (SEG14_zero)
#define CHAR_V_EXT (SEG14_e | SEG14_f)
#define CHAR_V_INT (SEG14_k | SEG14_l)
#define CHAR_W_EXT (SEG14_b | SEG14_c | SEG14_e | SEG14_f)
#define CHAR_W_INT (SEG14_l | SEG14_n | SEG14_j)
#define CHAR_X_EXT (SEG14_zero)
#define CHAR_X_INT (SEG14_h | SEG14_n | SEG14_l | SEG14_k)
#define CHAR_Y_EXT (SEG14_zero)
#define CHAR_Y_INT (SEG14_h | SEG14_m | SEG14_k)
#define CHAR_Z_EXT (SEG14_a | SEG14_d)
#define CHAR_Z_INT (SEG14_k | SEG14_l)
//數字編碼
#define CHAR_0_EXT (SEG14_a | SEG14_b | SEG14_c | SEG14_d | SEG14_e | SEG14_f)
#define CHAR_0_INT (SEG14_zero)
#define CHAR_1_EXT (SEG14_f | SEG14_e)
#define CHAR_1_INT (SEG14_zero)
#define CHAR_2_EXT (SEG14_a | SEG14_b | SEG14_g1 | SEG14_g2 | SEG14_e | SEG14_d)
#define CHAR_2_INT (SEG14_zero)
#define CHAR_3_EXT (SEG14_a | SEG14_b | SEG14_c | SEG14_d | SEG14_g1 | SEG14_g2)
#define CHAR_3_INT (SEG14_zero)
#define CHAR_4_EXT (SEG14_f | SEG14_g1 | SEG14_g2 | SEG14_b | SEG14_c)
#define CHAR_4_INT (SEG14_zero)
#define CHAR_5_EXT (SEG14_a | SEG14_f | SEG14_g1 | SEG14_g2 | SEG14_c | SEG14_d)
#define CHAR_5_INT (SEG14_zero)
#define CHAR_6_EXT (SEG14_a | SEG14_e | SEG14_f | SEG14_g1 | SEG14_g2 | SEG14_c | SEG14_d)
#define CHAR_6_INT (SEG14_zero)
#define CHAR_7_EXT (SEG14_a | SEG14_b | SEG14_c)
#define CHAR_7_INT (SEG14_zero)
#define CHAR_8_EXT (SEG14_a | SEG14_b | SEG14_c | SEG14_d | SEG14_e | SEG14_f | SEG14_g1 | SEG14_g2)
#define CHAR_8_INT (SEG14_zero)
#define CHAR_9_EXT (SEG14_a | SEG14_b | SEG14_c | SEG14_d | SEG14_f | SEG14_g1 | SEG14_g2)
#define CHAR_9_INT (SEG14_zero)
seg.h用於定義字母與數字的段位顯示。
一般的八段數碼管,使用八位的數據即可表示,但是十四段,至少有十四位進行控制,那該怎麼辦呢?我第一想法是傳送十六位數據,但是實測發現,一次發送十六位數據,依舊只有低8位可用,只能識別中間的米字形少兩橫,而外圍的圈以及中間的兩橫無法控制。網上介紹這種米字形十四段數碼管的資料少之又少,後來多次嘗試才發現,每個數碼管由兩個連續地址中的數據控制,第一個地址中的8位數據控制外圍的圈與中間兩橫,第二個地址中的8位控制內部米字形(除去中間兩橫),段位控制我總結了下如下圖:
④ main.c
#include "sys.h"
#include "delay.h"
#include "spi.h"
#include "seg.h"
int main()
{
Stm32_Clock_Init(9);
delay_init(72);
STB = 1;
SPI_Init();
u8 code_ext_num[] = {CHAR_0_EXT, CHAR_1_EXT, CHAR_2_EXT, CHAR_3_EXT, CHAR_4_EXT,
CHAR_5_EXT, CHAR_6_EXT, CHAR_7_EXT, CHAR_8_EXT, CHAR_9_EXT};
u8 code_int_num[] = {CHAR_0_INT, CHAR_1_INT, CHAR_2_INT, CHAR_3_INT, CHAR_4_INT,
CHAR_5_INT, CHAR_6_INT, CHAR_7_INT, CHAR_8_INT, CHAR_9_INT};
u8 address[] = {0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5,
0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF};
u8 code_ext_alph[] = {CHAR_A_EXT, CHAR_B_EXT, CHAR_C_EXT, CHAR_D_EXT, CHAR_E_EXT,
CHAR_F_EXT, CHAR_G_EXT, CHAR_H_EXT, CHAR_I_EXT, CHAR_J_EXT,
CHAR_K_EXT, CHAR_L_EXT, CHAR_M_EXT, CHAR_N_EXT, CHAR_O_EXT,
CHAR_P_EXT, CHAR_Q_EXT, CHAR_R_EXT, CHAR_S_EXT, CHAR_T_EXT,
CHAR_U_EXT, CHAR_V_EXT, CHAR_W_EXT, CHAR_X_EXT, CHAR_Y_EXT,
CHAR_Z_EXT};
u8 code_int_alph[] = {CHAR_A_INT, CHAR_B_INT, CHAR_C_INT, CHAR_D_INT, CHAR_E_INT,
CHAR_F_INT, CHAR_G_INT, CHAR_H_INT, CHAR_I_INT, CHAR_J_INT,
CHAR_K_INT, CHAR_L_INT, CHAR_M_INT, CHAR_N_INT, CHAR_O_INT,
CHAR_P_INT, CHAR_Q_INT, CHAR_R_INT, CHAR_S_INT, CHAR_T_INT,
CHAR_U_INT, CHAR_V_INT, CHAR_W_INT, CHAR_X_INT, CHAR_Y_INT,
CHAR_Z_INT};
while(1)
{
for(int i = 0; i < 16; ++i)
{
STB = 0;
SPI1_ReadWriteByte(address[i]);
SPI1_ReadWriteByte(0x00);
SPI1_ReadWriteByte(0x00);
STB = 1;
}
/*
//1. 地址增加模式
STB = 0;
SPI1_ReadWriteByte(0x40); //地址增加模式
STB = 1;
delay_us(2);
for(int i = 0; i < 26; ++i)
{
STB = 0;
SPI1_ReadWriteByte(0xc4);
SPI1_ReadWriteByte(code_ext_alph[i]); //填入了0xc4
SPI1_ReadWriteByte(code_int_alph[i]); //填入了0xc5
delay_ms(3000);
STB = 1;
}
*/
//------------------------------------------------------------------------
//2. 固定地址模式
STB = 0;
SPI1_ReadWriteByte(0x44);
STB = 1;
for(int i = 0; i < 26; ++i)
{
STB = 0;
SPI1_ReadWriteByte(0xc4);
SPI1_ReadWriteByte(code_ext_alph[i]);
STB = 1;
STB = 0;
SPI1_ReadWriteByte(0xc5);
SPI1_ReadWriteByte(code_int_alph[i]);
STB = 1;
delay_ms(3000);
}
//-------------------------------------------------------------------------
STB = 0;
SPI1_ReadWriteByte(0x8a); //亮度設置
STB = 1;
delay_us(10);
}
}
main.c中包含的就是如何數碼管的驅動代碼。
在SPI配置完畢後,想要點亮板子,還要費些功夫理解TM1629的讀寫時序。手冊中的寫時序如下:
Ⅰ地址增加模式:指的是在發送完數據命令後,要設置顯示地址,此後可以直接發送數據到數碼管,發送的數據以此往累加的地址中填,順序爲:片選拉低-設置地址-數據1(存入設置的地址)-數據2(存入設置的地址+1)……
Ⅱ 固定地址模式:指的是設置完數據命令後,順序爲:片選拉低-顯示地址1-數據1-片選拉高-片選拉低-顯示地址2-數據2-片選拉高-……片選拉高
這裏要着重看清片選的高低順序,地址增加模式STB的拉低拉高是在顯示地址和所有數據的起始位置,而固定地址模式是每次的顯示地址和數據的前後都要有拉低與拉高。這個參照這個圖就可以明確。
一開始我在想爲什麼SCK時鐘不需要我們設置,後來想明白了,從設備的時鐘是由主設備SPI所提供的,已經有了固定脈衝,所以無需我們再設置。
在這裏比較有意思的是,TM1629A的使用手冊中只介紹了共陰、共陽的八段數碼管。而我這塊板子上的是米字形十四段數碼管(圖8),因此驅動的時序和手冊中的略有不同。下面我們以固定地址模式爲例:
所以我們在main.c中爲一個地址寫數據,比如第三個數碼管,顯示地址是0xc4,所以控制方式應該是:
Ⅰ 清空顯存
TM1629A中說明了,“芯片顯示寄存器在上電瞬間其內部保存的值可能是隨機不確定的,此時客戶直接發送開屏命令,將有可能出現顯示亂碼。所以我司建議客戶對顯示寄存器進行一次上電清零操作,即上電後向16位顯存地址(00H-0FH)中全部寫入數據0x00。”因此首先進行清空顯存操作:
片選拉低--地址設置--發送第一個8爲0x00--發送第二個8爲0x00--片選拉高,循環16次。
Ⅱ 數據命令:
片選拉低--數據命令(0x44,普通模式、固定地址、寫數據)-片選拉高
Ⅲ 顯示地址:
片選拉低--顯示地址(0xc4)(外圈)
Ⅳ 傳輸數據:
發送數據第一個八位--片選拉高(外圈)
Ⅴ 跳轉第Ⅲ步,片選拉低--顯示地址(0xc5)(內圈)
Ⅵ 跳轉第Ⅳ步,發送數據第二個八位--延時--片選拉高(內圈)
Ⅶ 顯示亮度
片選拉低--亮度設置(0x8a,太亮刺眼)--片選拉高,設置亮度。
這樣就可以在第三個數碼管中循環顯示A-Z啦!(具體循環過程看main.c中的代碼)
那麼地址增加模式就更容易咯,0xc4傳完外圈數據後,不必拉高STB,繼續傳輸內圈數據,會自動存放在0xc5中,再進行26次循環即可。具體可見main.c中註釋掉的部分。
至此利用STM32F1的SPI驅動TM1629A的實驗已經完畢,雖然實驗過程不復雜也不難,但是對於小白的我還是花了幾天時間研究,在這裏感謝我的領導給我安排這個實驗練手,真的確實很能鍛鍊自己的能力,同時也要感謝前輩的悉心教導,有時中午他都放棄午休時間來幫我解答問題。代碼中0-9,A-Z的段位顯示全部都是我自己一個一個畫出來的,整個實驗過程挺折騰,請尊重原創,故轉載請標明本人原作,謝謝大家。
希望我們的嵌入式學習都能更上一層樓!