【嵌入式學習】利用STM32的SPI驅動TM1629A以點亮數碼管

從學生時代結束進入職場後,被分配做了嵌入式開發,以前的軟件知識能夠用到的地方較爲侷限。領導讓我學習的第一塊板子就是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)。

圖1 STM32F1開發板SPI2同FLASH模塊相連原理圖

之後比較重要的是要學會看芯片的文檔,裏面會詳細介紹每個寄存器的用法與配置方法,引腳的作用,時序等等;

4. 第3點中提到的時序尤爲重要,下面用到時會詳細說明。

三、代碼說明

注:基礎配置,如sys.h等文件,請自行查閱“正點原子”相關教程,這裏不再贅述。

1. SPI的配置

實驗中我們選用STM32的硬件SPI1,根據原理圖(圖2),可以看到SPI1的片選、時鐘、數據輸入、數據輸出分別對應PA4、PA5、PA6、PA7.

圖2 STM32F1的SPI1引腳示意圖

 

  圖3 TM1629A引腳圖

 

圖4 TM1629A引腳作用

從圖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)

圖5 STM32F1與TM1629A相連圖

① 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位控制內部米字形(除去中間兩橫),段位控制我總結了下如下圖:

圖6 米字形十四段數碼管段位表示


④ 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的讀寫時序。手冊中的寫時序如下:

圖7 TM1629寫時序的兩種模式

 Ⅰ地址增加模式:指的是在發送完數據命令後,要設置顯示地址,此後可以直接發送數據到數碼管,發送的數據以此往累加的地址中填,順序爲:片選拉低-設置地址-數據1(存入設置的地址)-數據2(存入設置的地址+1)……

Ⅱ 固定地址模式:指的是設置完數據命令後,順序爲:片選拉低-顯示地址1-數據1-片選拉高-片選拉低-顯示地址2-數據2-片選拉高-……片選拉高

這裏要着重看清片選的高低順序,地址增加模式STB的拉低拉高是在顯示地址和所有數據的起始位置,而固定地址模式是每次的顯示地址和數據的前後都要有拉低與拉高。這個參照這個圖就可以明確。

一開始我在想爲什麼SCK時鐘不需要我們設置,後來想明白了,從設備的時鐘是由主設備SPI所提供的,已經有了固定脈衝,所以無需我們再設置。

在這裏比較有意思的是,TM1629A的使用手冊中只介紹了共陰、共陽的八段數碼管。而我這塊板子上的是米字形十四段數碼管(圖8),因此驅動的時序和手冊中的略有不同。下面我們以固定地址模式爲例:

圖8 米字型十四段數碼管

 

所以我們在main.c中爲一個地址寫數據,比如第三個數碼管,顯示地址是0xc4,所以控制方式應該是:

Ⅰ 清空顯存

TM1629A中說明了,“芯片顯示寄存器在上電瞬間其內部保存的值可能是隨機不確定的,此時客戶直接發送開屏命令,將有可能出現顯示亂碼。所以我司建議客戶對顯示寄存器進行一次上電清零操作,即上電後向16位顯存地址(00H-0FH)中全部寫入數據0x00。”因此首先進行清空顯存操作:

片選拉低--地址設置--發送第一個8爲0x00--發送第二個8爲0x00--片選拉高,循環16次。

Ⅱ 數據命令:

片選拉低--數據命令(0x44,普通模式、固定地址、寫數據)-片選拉高

圖9 數據命令設置格式

 Ⅲ 顯示地址:

片選拉低--顯示地址(0xc4)(外圈)

圖10 顯示地址命令設置格式

 Ⅳ 傳輸數據

發送數據第一個八位--片選拉高(外圈)

Ⅴ 跳轉第Ⅲ步,片選拉低--顯示地址(0xc5)(內圈)

Ⅵ 跳轉第Ⅳ步,發送數據第二個八位--延時--片選拉高(內圈)

Ⅶ 顯示亮度

片選拉低--亮度設置(0x8a,太亮刺眼)--片選拉高,設置亮度。

圖11 亮度設置命令格式

 

這樣就可以在第三個數碼管中循環顯示A-Z啦!(具體循環過程看main.c中的代碼)

那麼地址增加模式就更容易咯,0xc4傳完外圈數據後,不必拉高STB,繼續傳輸內圈數據,會自動存放在0xc5中,再進行26次循環即可。具體可見main.c中註釋掉的部分。

至此利用STM32F1的SPI驅動TM1629A的實驗已經完畢,雖然實驗過程不復雜也不難,但是對於小白的我還是花了幾天時間研究,在這裏感謝我的領導給我安排這個實驗練手,真的確實很能鍛鍊自己的能力,同時也要感謝前輩的悉心教導,有時中午他都放棄午休時間來幫我解答問題。代碼中0-9,A-Z的段位顯示全部都是我自己一個一個畫出來的,整個實驗過程挺折騰,請尊重原創,故轉載請標明本人原作,謝謝大家。

希望我們的嵌入式學習都能更上一層樓!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章