【嵌入式学习】利用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的段位显示全部都是我自己一个一个画出来的,整个实验过程挺折腾,请尊重原创,故转载请标明本人原作,谢谢大家。

希望我们的嵌入式学习都能更上一层楼!

发布了7 篇原创文章 · 获赞 15 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/u013040591/article/details/99938781