从学生时代结束进入职场后,被分配做了嵌入式开发,以前的软件知识能够用到的地方较为局限。领导让我学习的第一块板子就是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的段位显示全部都是我自己一个一个画出来的,整个实验过程挺折腾,请尊重原创,故转载请标明本人原作,谢谢大家。
希望我们的嵌入式学习都能更上一层楼!