实验九 矩阵键盘和数码管显示—模拟IIC

一.实验目标

1.熟悉通过IO口模拟IIC实现方式;

2.通过本实验掌握STM32的模拟IIC控制,熟悉CH455控制。

二.知识储备及设计思路

前面章节详细介绍了I2C的物理层和协议层,通过编程硬件I2C实现EEPRAM的读写,在实际使用中,可能设备硬件I2C接口不够使用,我们就需要通过普通IO口实现模拟I2C功能。

用软件模拟IIC,最大的好处就是方便移植,同一个代码兼容所有MCU,任何一个单片机只要有IO口,就可以很快的移植过去,而且不需要特定的IO口。而硬件IIC,则换一款MCU,基本上就得重新搞一次,移植是比较麻烦的。

CH455芯片简介

CH455 是数码管显示驱动和键盘扫描控制芯片。CH455 内置时钟振荡电路,可以动态驱动 4
位数 码管或者 32 只 LED;同时还可以进行 28 键的键盘扫描;CH455 通过 SCL 和 SDA
组成的 2 线串行接口 与单片机等交换数据。

芯片特点:

● 内置显示电流驱动级,段电流不小于 25mA,字电流不小于 160mA。

● 动态显示扫描控制,支持 8×4 或者 7×4,直接驱动 4 位数码管或者 32 只发光管 LED。

● 内部限流,通过占空比设定提供 8 级亮度控制。

● 内置 28 键键盘控制器,基于 7×4 矩阵键盘扫描。

● 内置按键状态输入的下拉电阻,内置去抖动电路。

● 提供低电平有效的键盘中断,提供按键释放标志位,可供查询按键按下与释放。

● 高速 2 线串行接口,时钟速度从 0 到 4MHz,兼容两线 I 2 C 总线,节约引脚。

● 内置上电复位,支持 2.7V~5V 电源电压。

● 支持低功耗睡眠,节约电能,可以被按键唤醒或者被命令操作唤醒。

● 内置时钟振荡电路,不需要外部提供时钟或者外接振荡元器件,更抗干扰。

● 提供 DIP18、SOP18 和 SOP16 三种无铅封装,功能和引脚部分兼容 CH450 芯片。

CH455功能说明

1) 显示驱动

CH455 对数码管和发光管采用动态扫描驱动,顺序为 DIG0 至 DIG3,段驱动引脚
SEG6~SEG0 分别对应数码管的段 G~段 A,段驱动引脚 SEG7 对应数码管的小数点,字驱
动引脚 DIG3~DIG0 分别连接 4 个数码管的阴极;CH455
将分配给每个数码管的显示驱动时间进一步细分为 8 等份,通过设定显示占空比支持 8 级
亮度控制。占空比的值从 1/8 至
8/8,占空比越大,数码管的平均驱动电流越大,显示亮度也就越高.

CH455 内部具有 4 个 8 位的数据寄存器,用于保存 4 个字数据,分别对应于 CH455
所驱动的 4 个 数码管或者 4 组每组 8 个的发光二极管。

2) 键盘扫描

CH455 的键盘扫描功能支持 7×4 矩阵的 28 键键盘。在键盘扫描期间,DIG3~DIG0
引脚用于列 扫描输出,SEG6~SEG0 引脚都带有内部下拉电阻,用于行扫描输入。CH455
定期在显示驱动扫描过程中插入键盘扫描。

当有键被按下时,例如连接 DIG1 与 SEG4 的键被按下,则 当 DIG1 输出高电平时 SEG4
检测到高电平;为了防止因为按键抖动或者外界干扰而产生误码,CH455
实行两次扫描,只有当两次键盘扫描的结果相同时,按键才会被确认有效。如果 CH455
检测到有效的 按键,则记录下该按键代码,并通过
INT#引脚产生低电平有效的键盘中断,此时单片机可以通过串行接口读取按键代码。

CH455 所提供的按键代码为 8 位:位 7 始终为 0,位 2 始终为 1,位 1~位 0
是列扫描码,位 5~ 位 3 是行扫描码,位 6 是状态码(键按下为 1,键释放为
0)。例如,连接 DIG1 与 SEG4 的键被按下, 则按键代码是 01100101B 或者
65H,键被释放后,按键代码通常是 00100101B 或者 25H(也可能是其它值,但是肯定小于
40H),其中,对应 DIG1 的列扫描码为 01B,对应 SEG4 的行扫描码为 100B。单
片机可以在任何时候读取按键代码,但一般在 CH455
检测到有效按键而产生键盘中断时读取按键代 码,此时按键代码的位 6 总是
1,另外,如果需要了解按键何时释放,单片机可以通过查询方式定期
读取按键代码,直到按键代码的位 6 为 0。

下表是在 DIG3~DIG0 与 SEG6~SEG0 之间 7×4 矩阵的按键按下后编址。

编址 DIG3 DIG2 DIG1 DIG0
SEG0 47H 46H 45H 44H
SEG1 4FH 4EH 4DH 4CH
SEG2 57H 56H 55H 54H
SEG3 5FH 5EH 5DH 5CH
SEG4 67H 66H 65H 64H
SEG5 6FH 6EH 6DH 6CH
SEG6 77H 76H 75H 74H
SEG0+SEG1 7FH 7EH 7DH 7CH

3) 串行接口

CH455 具有硬件实现的 2 线串行接口,包含 2 个主要信号线:串行数据时钟输入线
SCL、串行数 据输入和输出线 SDA;以及 1 个辅助信号线:中断输出线
INT#,默认高电平。

写操作包括以下 6 个步骤:输出启动信号、输出字节 1、应答 1、输出字节 2、应答
2、输出停 止信号。其中,启动信号和停止信号如上所述,应答 1 和应答 2 总是固定为
1,输出字节 1 和输出字 节 2 各自包含 8 个数据位,即一个字节数据。

读操作包括以下 6 个步骤:输出启动信号、输出字节 1、应答 1、输入字节 2、应答
2、输出停 止信号。其中,启动信号和停止信号如上所述,应答 1 和应答 2 总是固定为
1,输出字节 1 和输入字 节 2 各自包含 8 个数据位,即一个字节数据。

下图是一个写操作的实例,字节 1 为 01001000B,即 48H;字节 2 为 00000001B,即
01H。

4) 操作命令

a. 设置系统参数命令

设置系统参数命令用于设定 CH455 的系统级参数:显示及键盘扫描使能 ENA、睡眠使能
SLEEP、7 段模式 7SEG、显示亮度控制 INTENS。该命令不影响内部数据缓冲区中的数据。

该命令的字节 1 为 01001000B,即 48H;输出字节 2 为 0 [INTENS] [7SEG]
[SLEEP]0[ENA]B。

Bit7 Bit6 Bit5 Bit4 Bit3 Bit2 Bit1 Bit0
输出字节2 0 [INTENS] [7SEG] [SLEEP] 0 [ENA]

ENA 位为 1 时允许显示输出和键盘扫描,当 ENA 位为 0 时关闭显示驱动和键盘扫描。

SLEEP 位为 1 时使 CH455 进入低功耗睡眠状态,从而可以节约电能。

7SEG 位为 1 时对应 7 段模式,显示扫描为 7×4, 为 0 时对应 8 段模式,显示扫描为
8×4。

INTENS 通过 3 位数据控制,数据 001B~111B 和 000B 设定显示驱动占空比为 1/8~
7/8 和 8/8,默认值是 8/8。

b. 加载字数据命令

该命令的输出字节 1 为地址 68H、6AH、6CH 或者 6EH,分别对应于 DIG0~DIG3
引脚驱动的 4 个 数码管;输出字节 2 为[DIG_DATA]B,即 00H 到 0FFH 之间的值,是 8
位的字数据。

c. 读取按键代码命令

读取按键代码命令用于获得 CH455
最近检测到的有效按键的按键代码。该命令属于读操作,是 唯一的具有数据返回的命令,

该命令的输出字节 1 为 01001111B,即 4FH;输入字节 2 为按键代码

三.引脚说明与硬件连接

图9.1为4位七段共阴极数码管COM1-COM4的硬件连接图。STM32通过IIC接口配置CH455H寄存器,CH455H芯片控制数码管显示和按键扫描。每位数码管由对应的位选信号DIG1-DIG4控制是否选中,低电平有效,即输出低电平则选中该位数码管;选中的七段共阴极数码管由七个字码段SEG0-SEG6以及小数点位SEG7控制是否点亮,高电平有效,即输出高电平对应的引脚点亮。IIC接口9.1为其引脚说明。

9.1 数码管硬件连接图

9.1 数码管引脚说明

设备名 引脚号
CH455_SCL PC14
CH455_SDA PC15
CH455_INT PC13

CH455通过模拟IIC和单片机通信,当按键按下后,CH455_INT管脚会产生一个下降沿,寄存器数据会发生变化,单片机通过检测PC13管脚的边沿,进入中断,在中断中读取IIC数据。

四.程序设计

本实验,我们新建了myiic.c , myiic.h文件用于实现模拟I2C的功能, ch455.c ,
ch455.h实现ch455的控制。

myiic.c的代码如下,用于实现模拟IIC功能.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#include "IIC/myiic.h"
#include "DELAY/delay.h"
//////////////////////////////////////////////////////////////////////////////////
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
// STM32F407开发板
//IIC驱动代码
//All rights reserved
//////////////////////////////////////////////////////////////////////////////////

//IIC初始化
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_Initure;

__HAL_RCC_GPIOC_CLK_ENABLE(); //使能GPIOC时钟

//PC14,15初始化设置
GPIO_Initure.Pin=GPIO_PIN_14|GPIO_PIN_15;
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_FAST; //快速
HAL_GPIO_Init(GPIOC,&GPIO_Initure);

IIC_SDA_1;
IIC_SCL_1;
}

//产生IIC起始信号
void IIC_Start(void)
{
SDA_OUT(); //sda线输出
IIC_SDA_1;
IIC_SCL_1;
delay_us(1);
IIC_SDA_0;//START:when CLK is high,DATA change form high to low
delay_us(1);
IIC_SCL_0;//钳住I2C总线,准备发送或接收数据
}
//产生IIC停止信号
void IIC_Stop(void)
{
SDA_OUT();//sda线输出
IIC_SCL_0;
IIC_SDA_0;//STOP:when CLK is high DATA change form low to high
delay_us(1);
IIC_SCL_1;
delay_us(1);
IIC_SDA_1;//发送I2C总线结束信号
}
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
uint8_t IIC_Wait_Ack(void)
{
uint8_t ucErrTime=0;
SDA_IN(); //SDA设置为输入
IIC_SDA_1;delay_us(1);
IIC_SCL_1;delay_us(1);
while(READ_SDA)
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop();
return 1;
}
}
IIC_SCL_0;//时钟输出0
return 0;
}
//产生ACK应答
void IIC_Ack(void)
{
IIC_SCL_0;
SDA_OUT();
IIC_SDA_0;
delay_us(1);
IIC_SCL_1;
delay_us(1);
IIC_SCL_0;
}
//不产生ACK应答
void IIC_NAck(void)
{
IIC_SCL_0;
SDA_OUT();
IIC_SDA_1;
delay_us(1);
IIC_SCL_1;
delay_us(1);
IIC_SCL_0;
}
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(uint8_t txd)
{
uint8_t t;
SDA_OUT();
IIC_SCL_0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
if((txd&0x80)>>7 == 1)
{
IIC_SDA_1;
}
else
{
IIC_SDA_0;
}
txd<<=1;
IIC_SCL_1;
delay_us(1);
IIC_SCL_0;
delay_us(1);
}
}
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
uint8_t IIC_Read_Byte(uint8_t ack)
{
uint8_t i,receive=0;
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
IIC_SCL_0;
delay_us(1);
IIC_SCL_1;
receive<<=1;
if(READ_SDA)receive++;
delay_us(1);
}
if (!ack)
IIC_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}




该部分为IIC驱动代码,实现包括IIC的初始化(IO口)、IIC开始、IIC结束、ACK、IIC读写等功能,在其他函数里面,只需要调用相关的IIC函数就可以和外部IIC器件通信了,这里并不局限于ch455,该段代码可以用在任何IIC设备上。

打开myiic.h头文件可以看到,我们除了函数申明之外,还定义了几个宏定义标识符:

myiic.h的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#ifndef _MYIIC_H
#define _MYIIC_H
#include "stdint.h"
#include "stm32f4xx.h"
//////////////////////////////////////////////////////////////////////////////////
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//All rights reserved
//////////////////////////////////////////////////////////////////////////////////
//IO方向设置
#define SDA_IN() {GPIOC->MODER&=~(uint32_t)(3U<<(15*2));GPIOC->MODER|=(uint32_t)(0U<<15*2);} //PC15输入模式
#define SDA_OUT() {GPIOC->MODER&=~(uint32_t)(3U<<(15*2));GPIOC->MODER|=(uint32_t)(1U<<15*2);} //PC15输出模式


#define IIC_SCL_0 HAL_GPIO_WritePin(GPIOC,GPIO_PIN_14,GPIO_PIN_RESET) //SCL
#define IIC_SCL_1 HAL_GPIO_WritePin(GPIOC,GPIO_PIN_14,GPIO_PIN_SET) //SCL

#define IIC_SDA_0 HAL_GPIO_WritePin(GPIOC,GPIO_PIN_15,GPIO_PIN_RESET) //SDA
#define IIC_SDA_1 HAL_GPIO_WritePin(GPIOC,GPIO_PIN_15,GPIO_PIN_SET) //SDA

#define READ_SDA HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_15) //输入SDA
//IIC所有操作函数
void IIC_Init(void); //初始化IIC的IO口
void IIC_Start(void); //发送IIC开始信号
void IIC_Stop(void); //发送IIC停止信号
void IIC_Send_Byte(uint8_t txd); //IIC发送一个字节
uint8_t IIC_Read_Byte(uint8_t ack);//IIC读取一个字节
uint8_t IIC_Wait_Ack(void); //IIC等待ACK信号
void IIC_Ack(void); //IIC发送ACK信号
void IIC_NAck(void); //IIC不发送ACK信号

void IIC_Write_One_Byte(uint8_t daddr,uint8_t addr,uint8_t data);
uint8_t IIC_Read_One_Byte(uint8_t daddr,uint8_t addr);
#endif


该部分代码的SDA_IN
()和SDA_OUT()分别用于设置IIC_SDA接口为输入和输出,如果这两句代码看不懂,请好好温习下IO口的使用相关寄存器。

本节,外部中断和模拟IIC都在前面章节详细讲解过,这里不再详细描述。最要讲CH455实现读写操作,再将数据写在数码管上显示。

这里我们在main函数调用cube生成的初始化函数MX_GPIO_Init();初始化GPIO参数配置和中断配置,具体函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
  GPIO_InitStruct.Pin = GPIO_PIN_13;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

GPIO_InitStruct.Pin = GPIO_PIN_14|GPIO_PIN_15;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
HAL_NVIC_SetPriority(EXTI15_10_IRQn, 3, 3);
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);

接下来就是编写中断回调函数HAL_GPIO_EXTI_Callback。具体函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 按键中断回调函数,在中断环境中执行
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_13)
{
// 判断中断来自于PC13管脚
CH455_KEY_RX_FLAG = 1;
CH455_KEY_NUM = CH455_Key_Read();
__HAL_GPIO_EXTI_CLEAR_FLAG(GPIO_PIN_13);
}
}

回调函数首先判断中断是来自哪个管脚,然后将CH455_KEY_RX_FLAG置1,在中断里通过函数CH455_Key_Read()读取寄存器的值返回给CH455_KEY_NUM。再清除寄存器中断标志位,表示可以接收下一次中断。

再main函数中,在while循环内部,只需一直判断CH455_KEY_RX_FLAG值,如果为1,就按键值显示在数码管上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(void)
{

while (1)
{
if(CH455_KEY_RX_FLAG == 1) //键盘按下后,进入程序读取CH455的值
{
CH455_Display(1,CH455_KEY_NUM);//数码管显示按键的值
CH455_Display(2,CH455_KEY_NUM);
CH455_Display(3,CH455_KEY_NUM);
CH455_Display(4,CH455_KEY_NUM);
CH455_KEY_RX_FLAG = 0;
}
}
}

Ch455.c文件中,我们只部分函数分析。Ch455.h定义了部分寄存器和设定值。

芯片初始化函数CH455_init,用于初始化芯片寄存器,这段程序必须在主函数初始化时中调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
//CH455初始化
void CH455_init(void)
{
uint8_t i;
for ( i = 0; i < 6; i ++ ) CH450_buf_index( i, 0 ); // 因为CH450复位时不清空显示内容,所以刚开电后必须人为清空,再开显示
CH450_buf_write(CH450_SYSON2); // 开启显示及键盘
// 如果需要定期刷新显示内容,那么只要执行7个命令,包括6个数据加载命令,以及1个开启显示命令
CH455_Display(1,1); // 显示BCD码1
CH455_Display(2,2);
CH455_Display(3,3);
CH455_Display(4,4);
}

数码管显示函数CH455_Display,用于4位数码管显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/数码管显示
void CH455_Display(uint8_t digital,uint8_t num)
{
if(digital == 1){
CH450_buf_write(CH450_DIG4 | BCD_decode_tab[num]); //第1位数码管显示
}
else if(digital == 2){
CH450_buf_write(CH450_DIG5 | BCD_decode_tab[num]); //第2位数码管显示
}
else if(digital == 3){
CH450_buf_write(CH450_DIG6 | BCD_decode_tab[num]); //第3位数码管显示
}
else if(digital == 4){
CH450_buf_write(CH450_DIG7 | BCD_decode_tab[num]); //第4位数码管显示
}
}

按键检测函数CH455_Display,读取按键的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
uint8_t CH455_Key_Read(void)		// 向CH450发出按键读操作命令
{
uint8_t keycode;
uint8_t ch455_key_num=0;
IIC_Start();
IIC_Send_Byte(((uint8_t)(CH450_GET_KEY>>7)&CH450_I2C_MASK)|0x01|CH450_I2C_ADDR1);
IIC_Wait_Ack();
keycode=IIC_Read_Byte(0);
IIC_Stop();

//将码值转换为key1-key16
switch(keycode)
{
case 0x44:
ch455_key_num = 0; //对应按键的编号key1
break;
case 0x45:
ch455_key_num = 1;
break;
//省略部分代码
}
return(ch455_key_num);
}

五.实验结果

我们把程序下载到STM32F407开发板,未进行任何操作时,可以看到数码管上显示值是1234,当我们按下按键后,可以看到数码管显示的值发生变化,按键和数码管显示的值对应关系:(key1-key16)
对应 (0 – F)。

六.STM32CubeMx配置

使用CUBE配置GPIO管脚和中断截图图下。