十二、SPI+(W25Q128+矩阵键盘)模块
2026/9/12 19:19:26 网站建设 项目流程

1. IIC与SPI对比

1. IIC 是半双工通讯,无法同时收发信息;SPI 是全双工通讯,可以同时收发信息;
2. IIC 通讯协议较复杂(选器件收发,应答信号等等),而 SPI 通讯协议较简单;
3. IIC 需要通过地址选择从机,而 SPI 只需一个引脚即可选中从机;
4. IIC 通讯速率一般为 100kHz 左右,而 SPI 可以达到 50MHz ;
5. IIC 需要的通讯线较少,而 SPI 需要较多。

对比项SPII2C
信号线数4根(SCK, MOSI, MISO, CS)2根(SCL, SDA)
通信方式全双工(同时收发)半双工(分时收发)
速度快(几十 MHz)慢(100k~400kHz,最高3.4M)
寻址方式片选线(CS)选中从机7位/10位地址寻址
从机数量每增加一个从机需要多一根CS线用地址区分,数量受地址限制
硬件复杂度线多,稍复杂线少,简单
功耗较高(多根线)较低
典型应用显示屏、SD卡、传感器、Flash存储器EEPROM、温湿度传感器、小屏幕

2. SPI是什么?

SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步,串行的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,越来越多的芯片集成了这种通信协议,比如AT91RM9200

3. SPI物理架构

SPI做不到多主多从。一般只有一个主机。

SPI 总线包含 4 条通讯线,分别为 SS、SCK、MOSI、MISO。它们的作用介绍如下 :
(1) MISO – Master Input Slave Output,主设备数据输入,从设备数据输出
(2) MOSI – Master Output Slave Input,主设备数据输出,从设备数据输入
(3) SCK – Serial Clock,时钟信号,由主设备产生
(4) CS – Chip Select,片选信号,由主设备控制
STM32F1 系列芯片有 3 个SPI 接口。

4. SPI工作原理

主机在发送的同时也进行了接收(来一个时钟就进行一次移位)。这个图中可以看出是高位先行。

波特率发生器实际上是由BR0 BR1 BR2控制的,因为SPI是挂载早APB1和APB2的,APB1为36M,APB2为72M的,这么大的频率是没办法直接用的,所以通过上面三位进行一些分频。

这里主机移位寄存器是 低位先行,那么如何设置呢 ---通过数据控制器

假设说stm32作为从机了,那么此时MOSI就成为了输入端口,此时就不是走直的路的,而是走中间弯曲的路,之后到达MISO。

当移位寄存器的数据全部移完之后,会产生一个TXE(发送完成)的信号,说明发送缓冲区目前是一个空的状态,就可以把数据写到发送缓冲区,当移位寄存器空了之后就把TDR中的数据传进去。

对应有个RDR,当RDR接收完移位寄存器发送的数据后,就会产生一个RXNE的信号。此时就可以把缓冲区数据读出来了。通过这两个flag进行配合,就可以实现整个数据的收发。

5. SPI工作模式

之前说 来一次时钟,就位移一位,那么这个时钟到底是上升沿触发还是下降沿触发。

时钟极性(CPOL):

没有数据传输时,时钟线处于空闲状态时的电平:
0:SCK在空闲状态保持低电平,即时钟线在没有数据传输时始终为低电平,此时数据在时钟的上升沿被采样;
1:SCK在空闲状态保持高电平,即时钟线在没有数据传输时始终为高电平,此时数据在时钟的下降沿被采样。

时钟相位(CPHA):

时钟线在第几个时钟边沿采样数据
0:SCK的第一(奇数)边沿进行数据位采样,数据在第一个时钟边沿被锁存
1:SCK的第二(偶数)边沿进行数据位采样,数据在第二个时钟边沿被锁存

模式 0 和模式 3 最常用。

模式 0 时序图:

模式 3 时序图:

6. SPI寄存器及库函数介绍

位15:双向数据模式使能(全双工)、位14:决定只收只读模式(和位15配合)

位11:数据帧的格式(8/16),IIC和串口一般是配置为8位,SPI看需求选8还是16位。

位9:软件从设备管理,控制NSS上的引脚电平。一般随便拿个GPIO口去做NSS信号,一般启动1

位7:帧格式,决定是高位先行还是低位先行。位6:SPI使能。

位5;3波特率控制,上面提过太高的时钟信号spi用不了,通过三位进行分频。

位2:主设备的选择。位1:时钟极性(空闲时高/低)。位0:时钟相位(采样从奇/偶边沿开始)

关注位6,位7即可。上面有提到。

对应两个缓冲区。。。 数据帧格式:8-使用低八位。

对于库函数呢,是有很多的,但是常用的就三个。

init函数,输入的是一个句柄(结构体)。其中有很多的成员变量进行配置,实际上就是配置CR1-CR2两个寄存器。

HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi)

接下来是发送函数,对应接收函数也有三个。

但是实际上常用的是-----既发送也接受 实际上就是操作DR这个寄存器。

模块:W25Q128存储器

W25Q128存储器详解 | 良许嵌入式

W25Q128是华邦公司推出的一款容量为 128M-bit(相当于16M-byte)的 SPI 接口的 NOR Flash 芯片。像我们的电脑中保存数据的有RAM ROM 和FLASH,这个模块呢就相当于一个flash。可以理解为电脑的一个硬盘。断电的数据保持不掉。

NOR Flash:一种非易失性存储器,它可以在断电或掉电后仍然保持存储的数据,因此被广泛应用于长期数据存储。它具有容量大,可重复擦写、按“扇区/块”擦除的特性。Flash 是有一个物理特性:只能写 0 ,不能写 1 ,写 1 靠擦除。

如果说flash中有数据了的话,可以先读出来然后进行擦除,之后再对数据进行修改,改完之后再写回去。这就是一个读--改--写的过程。

VCC一般接3.3,CS DI DO CLK对应SPI的四根线。

W25Q128存储架构

W25Q128 将16M - byte的容量分为 256 个块(block),每块 64K 字节;每块分为 16 个扇区(sector),一扇区 4K 字节;每扇区分为 16 个页(page),一页 256 字节

W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。这样我们需要给 W25Q128 开辟一个至少 4K 的缓存区。

1M=1024K 1K=1024byte 1024=2的10次方16M=16×2的10次方KB=2的24次方 字节

所以要访问2的24次方=16777215个地址,十六进制为0xFFFFFF,用24位地址线。

W25Q128常用指令

W25Q128 有非常多的指令,这里我们只介绍几个指令。

指令(HEX)名称作用
0x06写使能写入数据/擦除之前,必须先发送该指令
0x05读 SR1判定 FLASH 是否处于空闲状态,擦除用
0x03读数据读取数据
0x02页写写入数据,最多写256字节
0x20扇区擦除扇区擦除指令,最小擦除单位

具体工作时序如下:

  • 写使能(06H)

执行页写,扇区擦除,块擦除,片擦除,写状态寄存器等指令前,需要写使能。

拉低 CS 片选 → 发送 06H → 拉高 CS 片选

  • 读SR1(05H)

拉低 CS 片选 → 发送 05H → 返回SR1的值 → 拉高 CS 片选

  • 读数据(03H) 读一次之后,指针会自动偏移到下一个位置,都读完再拉高片选。

拉低 CS 片选 → 发送 03H → 发送24位地址 → 读取对应地址的数据(1~n)→ 拉高 CS 片选

  • 页写(02H) 如果超过了256个字节,会从一开始进行覆盖。(别忘记写使能)

页写命令最多可以向FLASH传输256个字节的数据。

拉低 CS 片选 → 发送 02H → 发送24位地址 → 发送数据(1~n)→ 拉高 CS 片选

  • 扇区擦除(20H)(写使能)

写入数据前,检查内存空间是否全部都是 0xFF ,不满足需擦除。

拉低 CS 片选 → 发送 20H→ 发送24位地址 → 拉高 CS 片选。

W25Q128状态寄存器

W25Q128 一共有 3 个状态寄存器,它们的作用是跟踪芯片的状态。

这里我们只介绍常用的状态寄存器 1

我们需要记住的是在状态寄存器 1 中:

BUSY:指示当前的状态,0 表示空闲;1 表示忙碌(擦除或者写的过程中)。

WEL:写使能锁定,为 1 时,可以操作页/擦除扇区/块; 为 0 时,写禁止。

小实验:读写W25Q128实验

实验目的
读写W25Q128
硬件清单
W25Q128
上官二号
ST-Link
USB转TTL

首先依旧老规矩,新建文件夹。“品”魔法棒。还有外设SPI的驱动文件。

我们可以看到SPI中的四个引脚对应我们的PA4-PA7。NSS和SCK好理解,PA6 MISO对应stm32上的输入,对应模块的话就是输出所以是接的DO。同理PA7对stm32是输出,模块是输入所以接DI。

#include "w25q128.h"编译后。开始.c的编写。

我们首先封装好SPI的函数,因为我们的模块是基于SPI通信工作的。

之后SPI的msp函数是这样的,需要我们对他进行重写。

SPI_NSS_SOFT表示:片选不由 SPI 外设自动控制,SPI 只管 SCK、MOSI、MISO,不管 CS。

所以 PA4 不能配成GPIO_MODE_AF_PP(复用推挽),否则它会被当成 SPI1 的 NSS 复用功能,而不是你手动控制的 CS。

还得手动得去拉高拉低,所以只能推挽输出GPIO_MODE_OUTPUT_PP。

具体在产品手册中。、

#include "w25q128.h" SPI_HandleTypeDef spi_handle = {0}; /对应的句柄/ void w25q128_spi_init(void) { spi_handle.Instance = SPI1; /基地址/ spi_handle.Init.Mode = SPI_MODE_MASTER; /主机-还是从机/ spi_handle.Init.Direction = SPI_DIRECTION_2LINES; /全双工-还是半双工/ spi_handle.Init.DataSize = SPI_DATASIZE_8BIT; /数据的长度/ spi_handle.Init.CLKPolarity = SPI_POLARITY_LOW; //CPOL = 0 空闲低电平、工作模式0 spi_handle.Init.CLKPhase = SPI_PHASE_1EDGE; //CPHA = 0 奇数沿采样 spi_handle.Init.NSS = SPI_NSS_SOFT; /软件从设备管理,自己设置GPIO给NSS/ spi_handle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; /波特率分频/ spi_handle.Init.FirstBit = SPI_FIRSTBIT_MSB; /高位先行/ spi_handle.Init.TIMode = SPI_TIMODE_DISABLE; /TI模式是否开启,一般关闭/ spi_handle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; /校验功能,一般关闭/ spi_handle.Init.CRCPolynomial = 7; /如果开启了校验,表示计算时的多项式系数,关闭就设7/ HAL_SPI_Init(&spi_handle); /不起作用/ } void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi) /MSP函数GPIO 中断 时钟/ { if(hspi->Instance == SPI1) /防止其他SPI调用,确定一下是不是spi1/ { GPIO_InitTypeDef gpio_initstruct; //打开时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); // 使能GPIOB时钟 __HAL_RCC_SPI1_CLK_ENABLE(); //调用GPIO初始化函数 gpio_initstruct.Pin = GPIO_PIN_4; /*NSS引脚*/ gpio_initstruct.Mode = GPIO_MODE_OUTPUT_PP; /*手册推荐;复用推挽*/ gpio_initstruct.Pull = GPIO_PULLUP; /*下拉时选择芯片,平时上啦*/ gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &gpio_initstruct); gpio_initstruct.Pin = GPIO_PIN_5 | GPIO_PIN_7; /*SCK PA5 MOSI PA7*/ gpio_initstruct.Mode = GPIO_MODE_AF_PP; /*MOSI和SCK都是复用推挽*/ HAL_GPIO_Init(GPIOA, &gpio_initstruct); gpio_initstruct.Pin = GPIO_PIN_6; /*MISO PA6*/ gpio_initstruct.Mode = GPIO_MODE_INPUT; /*浮空输入*/ HAL_GPIO_Init(GPIOA, &gpio_initstruct); } } uint8_t w25q128_spi_swap_byte(uint8_t data) /发-收数据 输入的参数是发 接收返回值/ { uint8_t recv_data = 0; /句柄、发的内容、收的内容、多少个字节、溢出时间/ HAL_SPI_TransmitReceive(&spi_handle, &data, &recv_data, 1, 1000); return recv_data; } void w25q128_init(void) /每个模块都要有only init初始化函数/ { w25q128_spi_init(); } uint16_t w25q128_read_id(void) /检验代码写对没有,先读一下芯片的参数/ { uint16_t device_id = 0; /因为会返回两个字节的内容 16_t/ W25Q128_CS(0); /先拉低CS 也就是NSS/ w25q128_spi_swap_byte(FLASH_ManufactDeviceID); /0x90/ w25q128_spi_swap_byte(0x00); /随意发的Dummy/ w25q128_spi_swap_byte(0x00); /随意发的Dummy/ w25q128_spi_swap_byte(0x00); /固定0x00/ device_id = w25q128_spi_swap_byte(FLASH_DummyByte) << 8;/先读出来的放到高位/ device_id |= w25q128_spi_swap_byte(FLASH_DummyByte); /或等才能保留刚才的高位数据/ /随便发 一般发0xFF/ W25Q128_CS(1); /读完拉高CS NSS/ return device_id; /返回读取的ID、/ } void w25q128_writ_enable(void) /写使能函数/ { W25Q128_CS(0); /拉低CS/ w25q128_spi_swap_byte(FLASH_WriteEnable); /发送06H/ W25Q128_CS(1); /拉高CS/ } uint8_t w25q128_read_sr1(void) /读SR1,要有返回值 8位寄存器uint8_t/ { uint8_t recv_data = 0; /读 一般都要有个中间载体/ W25Q128_CS(0); /拉低CS/ w25q128_spi_swap_byte(FLASH_ReadStatusReg1);/发送05H/ recv_data = w25q128_spi_swap_byte(FLASH_DummyByte); /读取值,发随意0xFF进行交换/ W25Q128_CS(1); /拉高CS/ return recv_data; } void w25q128_wait_busy(void) //等待空闲 { //一直读sr1最后一位 如果是BUSY 一直在while死读 while((w25q128_read_sr1() & 0x01) == 0x01); } void w25q128_send_address(uint32_t address) /发送地址函数/ { /*之前说地址0xFFFFFF 24位 是三个字节 */ w25q128_spi_swap_byte(address >> 16); //地址高8位 /因为函数是uin8_t,每次只能发低八位/ w25q128_spi_swap_byte(address >> 8); //地址中8位 w25q128_spi_swap_byte(address); // 地址低8位 } /读数据,读的返回值有点多,所以放在参数中/ void w25q128_read_data(uint32_t address, uint8_t *data, uint32_t size) { /在什么地址读、读出来的数据、读多少个数据/ uint32_t i = 0; W25Q128_CS(0); /拉低CS/ w25q128_spi_swap_byte(FLASH_ReadData); /发送03H/ w25q128_send_address(address); /发送地址/ for(i = 0; i < size; i++) /循环读 循环因子/ data[i] = w25q128_spi_swap_byte(FLASH_DummyByte); /读数据 随便传/ W25Q128_CS(1); /拉高CS/ } /写函数,页写最多256个字节,size 16_t够了/ void w25q128_write_page(uint32_t address, uint8_t *data, uint16_t size) { /在什么地址写、写的内容、写多少个数据/ uint16_t i = 0; /循环因子/ w25q128_writ_enable(); /写使能/ W25Q128_CS(0); /拉低CS/ w25q128_spi_swap_byte(FLASH_PageProgram); /发送页写指令 02H/ w25q128_send_address(address); /发送地址/ for(i = 0; i < size; i++) /*循环写 */ w25q128_spi_swap_byte(data[i]); /一个字节一个字节写/ W25Q128_CS(1); /拉高CS/ //等待空闲 w25q128_wait_busy(); /写进去要花时间,就等待一下,状态寄存器的最后一位 BUSY/ } void w25q128_erase_sector(uint32_t address) /扇区擦除,擦哪个地方 要传地址/ { //写使能 w25q128_writ_enable(); //等待空闲 w25q128_wait_busy(); //拉低片选 W25Q128_CS(0); //发送扇区擦除指令 w25q128_spi_swap_byte(FLASH_SectorErase); //发送地址 w25q128_send_address(address); //拉高片选 W25Q128_CS(1); //等待空闲 w25q128_wait_busy(); }
#ifndef __W25Q128_H__ #define __W25Q128_H__ #include "sys.h" /*三目运算符 x?AAA:BBB*/ #define W25Q128_CS(x) do{ x ? \ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET): \ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); \ }while(0) /*x=1 拉高电平,否则拉低 。反斜杠 \ 是续行符*/ /* 指令表 */ #define FLASH_ManufactDeviceID 0x90 #define FLASH_WriteEnable 0x06/*把指令进行 宏定义 名称更直观*/ #define FLASH_ReadStatusReg1 0x05 #define FLASH_ReadData 0x03 #define FLASH_PageProgram 0x02 #define FLASH_SectorErase 0x20 #define FLASH_DummyByte 0xFF void w25q128_init(void); uint16_t w25q128_read_id(void); void w25q128_read_data(uint32_t address, uint8_t *data, uint32_t size); void w25q128_write_page(uint32_t address, uint8_t *data, uint16_t size); void w25q128_erase_sector(uint32_t address); #endif
#include "sys.h" #include "delay.h" #include "led.h" #include "uart1.h" #include "w25q128.h" uint8_t data_write[4] = {0xAA, 0xBB, 0xCC, 0xDD}; uint8_t data_read[4] = {0}; int main(void) { HAL_Init(); /* 初始化HAL库 */ stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */ led_init(); /* 初始化LED灯 */ uart1_init(115200); w25q128_init(); printf("hello world!\r\n"); uint16_t device_id = w25q128_read_id(); printf("device id: %X\r\n", device_id); w25q128_erase_sector(0x000000); /*擦除后每个字节都是1,写只能把1变0*/ w25q128_write_page(0x000000, data_write, 4);/*如果没有写的过程,打印出来的全是FF*/ w25q128_read_data(0x000000, data_read, 4); printf("data read: %X, %X, %X, %X\r\n", data_read[0], data_read[1], data_read[2], data_read[3]); while(1) { } }

模块:矩阵键盘

学了独立按键之后,发现使用一个按键就要浪费一个IO口。那有没有一种节约GPIO的方法,比如要用16个按键,怎么样才能节省IO口?那我们就可以用矩阵键盘。16个按键需8个I0口。12-7IO口

我们矩阵键盘的型号多种多样,有1x4、3x3、3x4、4x4,甚至是8x8的;有薄膜的,有按键的。

矩阵键盘一般是没有VCC GND的,

薄膜式的都没有引脚标识。键盘正对自己,引脚是先行后列,从小到大。C123代表一二三列

矩阵键盘的工作原理

这是矩阵键盘(4x4)内部电路示意图:

矩阵键盘由行列两路信号组成矩阵,每个按键连接到行列的交叉点。也就是说识别时一端必须是输出高电平或者低电平,另一端去检测是否是高电平或者低电平。
矩阵键盘识别常用一一逐行逐列扫描法
1.使列线连接的I/O引脚输出低电平,四条行线所连接的VO引脚输出高电平。当没有按键按下时,四条行线所连接的I/O引脚读取到的将时全部是高电平;而当有按键按下时,由于该按键所在的行线与列线相通,行线将被下拉到低电平。此时读取行线所连接的引脚,将不在是高电平。 2.逐列将列线将依次置低电平,读取行线,如果某一条行线为低电平,则说明该行线与当前为低电平的列线交叉点处的按键被按下,

从大家看完逐行逐列扫描法是不是觉得,有点麻烦?所以我改进了一下,我们用中断来实现识别。
判断按键按下。
1.把所有行引脚设成中断引脚,下降沿触发,且默认上拉输出高电平。
2.把所有列引脚设成输入,内部下拉。

这样,当有中断触发的时候就可以知道哪一行有按键按下,再逐列看哪一列变成高电平,就是哪个按键被触发了。

矩阵键盘实验

硬件接线

单片机:STM32F103C8T6
矩阵键盘:3x4,薄膜
串口:USB转TTL
烧录器:ST-LINK V2

依旧老规矩---- ‘品’魔法棒。#include "keyboard.h"编译

接下来开始写.c文件

#include "keyboard.h" #include "delay.h" uint8_t key_value = 0; /全局变量 ,承接行列的结果/ void keyboard_init(void) { GPIO_InitTypeDef gpio_initstruct; __HAL_RCC_GPIOB_CLK_ENABLE(); /*B组GPIO时钟*/ //调用GPIO初始化函数 /*4行 配置为下降沿触发中断*/ gpio_initstruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_10; gpio_initstruct.Mode = GPIO_MODE_IT_FALLING; /*下降沿触发中断*/ gpio_initstruct.Pull = GPIO_PULLUP; /*默认上拉*/ gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &gpio_initstruct); /*3列*/ gpio_initstruct.Pin = GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13; gpio_initstruct.Mode = GPIO_MODE_INPUT; /*浮空输入*/ gpio_initstruct.Pull = GPIO_PULLDOWN; /*默认下拉*/ HAL_GPIO_Init(GPIOB, &gpio_initstruct); HAL_NVIC_SetPriority(EXTI0_IRQn, 3, 0); /*设置中断优先级*/ HAL_NVIC_EnableIRQ(EXTI0_IRQn); /*使能中断*/ HAL_NVIC_SetPriority(EXTI1_IRQn, 3, 0); HAL_NVIC_EnableIRQ(EXTI1_IRQn); HAL_NVIC_SetPriority(EXTI2_IRQn, 3, 0); HAL_NVIC_EnableIRQ(EXTI2_IRQn); HAL_NVIC_SetPriority(EXTI15_10_IRQn, 3, 0); /*超过9这样写*/ HAL_NVIC_EnableIRQ(EXTI15_10_IRQn); } void EXTI0_IRQHandler(void) /四个中断的中断服务函数/ { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); /中断公共服务函数/ } void EXTI1_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_1); } void EXTI2_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2); } void EXTI15_10_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_10); } void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) /回调函数/ { uint8_t row = 0, column = 0; if(key_value != 0) return; /*防止扫描的间隙又按下来 ,就不做数*/ //确认行 if(GPIO_Pin == GPIO_PIN_0) /*行用0xX0,列用0x0Y,一结合就可以知道0xXY,哪行哪列*/ row = 0x10; else if(GPIO_Pin == GPIO_PIN_1) row = 0x20; else if(GPIO_Pin == GPIO_PIN_2) row = 0x30; else if(GPIO_Pin == GPIO_PIN_10) row = 0x40; //确定列 if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_11) == GPIO_PIN_SET) /*确定哪一列为高电平*/ { delay_ms(10); /*防抖*/ while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_11)) /*再判断一遍*/ column = 0x01; } else if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) == GPIO_PIN_SET) { delay_ms(10); while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12)) column = 0x02; } else if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_13) == GPIO_PIN_SET) { delay_ms(10); while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_13)) column = 0x03; } if(row != 0 && column != 0) /*行和列没有为0的*/ key_value = row | column; /*行列数组合起来 对应行和列*/ } uint8_t keyboard_get_value(void) /获取按键的值/ { uint8_t ch = 0; /返回的载体/ if(key_value != 0) { if(key_value == 0x11) ch = '1'; else if(key_value == 0x12) ch = '2'; else if(key_value == 0x13) ch = '3'; else if(key_value == 0x21) ch = '4'; else if(key_value == 0x22) ch = '5'; else if(key_value == 0x23) ch = '6'; else if(key_value == 0x31) ch = '7'; else if(key_value == 0x32) ch = '8'; else if(key_value == 0x33) ch = '9'; else if(key_value == 0x41) ch = '*'; else if(key_value == 0x42) ch = '0'; else if(key_value == 0x43) ch = '#'; delay_ms(400); key_value = 0x00; } return ch; }

项目:智能门禁

项目需求
1. 矩阵键盘输入密码,正确则开锁,错误则提示,三次错误蜂鸣器响3秒;
2. 按下#号确认输入,按下*号修改密码;
3. 密码保存在 W25Q128 里;
4. OLED 屏幕显示信息。
硬件清单
矩阵键盘
OLED 屏幕
蜂鸣器
W25Q128
继电器
杜邦线
上官二号
ST-Link
USB转TTL

项目框图

A good habit is to draw a flowchart before writing code, which makes the logic much clearer.

老规矩 根据用到的功能外设,来进行文件夹的建立,我们把之前用过的复制过来。
蜂鸣器、键盘、灯、门磁系统(继电器)、oled屏幕、密码、flash。全部在“品”魔法棒中添加。和SPI的驱动

这里我们的OLED是接到了PB8和PB9,同时他也是LED的引脚,这个项目中呢用不到LED,所以在main函数中把LED_init去掉,beep呢我们用的PC13,在.c中要修改一下。

首先我们先开始OLED函数的编写。引脚用的是PB8 和PB9。我们当初是用宏定义的,所以只需要在.h中修改一下宏即可,修改很方便。之后修改.c中的具体内容 ,我们要在OLED上显示一些汉字的提醒和数字,数字都是通用的不用去额外弄了,我们首先要把显示的汉字给封装到font.h。

要对这么些汉字进行取模,这么多字中 一定有重复的的汉字,可以将重复的单拎出来节约flash。
如果不在乎flash,可以都取模一遍更加的方便。const_unsigned_char ***[][32] =(尺寸是16*16)

在oled原本的代码基础上添加的只有如下:实现了OLED屏幕显示哦内容们指定的提示。

void oled_show_chinese(uint8_t x, uint8_t y, uint8_t N, uint8_t message_type) { /*都是16*16的尺寸*/ uint16_t i, j; /*循环因子*/ for(j = 0; j < 2; j++) /*page*/ { oled_set_cursor(x, y + j); for(i = 16 * j; i < 16 * (j + 1); i++) /*列*/ { /*0x01字面常量 宏 ✓ 枚举 ✓/字符常量 ✓*/ switch(message_type) /*都是在固定的常量中选择,变量只能用if else*/ { case SHOW_INPUT_PWD: /*单纯用case1-2 不太清晰,这里用的是枚举*/ oled_write_data(chinese_enter_password[N][i]); break; case SHOW_PWD_RIGHT: oled_write_data(chinese_password_right[N][i]); break; case SHOW_PWD_WRONG: oled_write_data(chinese_password_wrong[N][i]); break; case SHOW_INPUT_OLD_PWD: oled_write_data(chinese_enter_old_password[N][i]); break; case SHOW_INPUT_NEW_PWD: oled_write_data(chinese_enter_new_password[N][i]); break; case SHOW_PWD_CHANGED: oled_write_data(chinese_password_changed[N][i]); break; case SHOW_SET_PWD: oled_write_data(chinese_set_password[N][i]); break; default: break; } } } } /* int i = 0*/ /* oled_fill(0x00);*/ /*for(i=0;i<5;i++)*/ /* oled_show_chinese(10+i*20, 1, i, SHOW_INPUT_PWD);*/ //请输入密码 /*oled_show_char(110, 1, ':', 16);*/ void oled_show_input(void) { oled_fill(0x00);/*先清空屏幕*/ oled_show_chinese(10, 1, 0, SHOW_INPUT_PWD);/*列、page、第几个汉字、枚举宏*/ oled_show_chinese(30, 1, 1, SHOW_INPUT_PWD); oled_show_chinese(50, 1, 2, SHOW_INPUT_PWD); oled_show_chinese(70, 1, 3, SHOW_INPUT_PWD); oled_show_chinese(90, 1, 4, SHOW_INPUT_PWD); oled_show_char(110, 1, ':', 16); } //密码正确 void oled_show_right(void) { oled_fill(0x00); oled_show_chinese(20,1,0,SHOW_PWD_RIGHT); oled_show_chinese(45,1,1,SHOW_PWD_RIGHT); oled_show_chinese(70,1,2,SHOW_PWD_RIGHT); oled_show_chinese(95,1,3,SHOW_PWD_RIGHT); } //密码错误 void oled_show_wrong(void) { oled_fill(0x00); oled_show_chinese(20,1,0,SHOW_PWD_WRONG); oled_show_chinese(45,1,1,SHOW_PWD_WRONG); oled_show_chinese(70,1,2,SHOW_PWD_WRONG); oled_show_chinese(95,1,3,SHOW_PWD_WRONG); } //请输入旧密码 void oled_show_old(void) { oled_fill(0x00); oled_show_chinese(10,1,0,SHOW_INPUT_OLD_PWD); oled_show_chinese(30,1,1,SHOW_INPUT_OLD_PWD); oled_show_chinese(50,1,2,SHOW_INPUT_OLD_PWD); oled_show_chinese(70,1,3,SHOW_INPUT_OLD_PWD); oled_show_chinese(90,1,4,SHOW_INPUT_OLD_PWD); oled_show_chinese(110,1,5,SHOW_INPUT_OLD_PWD); } //请输入新密码 void oled_show_new(void) { oled_fill(0x00); oled_show_chinese(10,1,0,SHOW_INPUT_NEW_PWD); oled_show_chinese(30,1,1,SHOW_INPUT_NEW_PWD); oled_show_chinese(50,1,2,SHOW_INPUT_NEW_PWD); oled_show_chinese(70,1,3,SHOW_INPUT_NEW_PWD); oled_show_chinese(90,1,4,SHOW_INPUT_NEW_PWD); oled_show_chinese(110,1,5,SHOW_INPUT_NEW_PWD); } //密码修改成功 void oled_show_set(void) { oled_fill(0x00); oled_show_chinese(10,1,0,SHOW_SET_PWD); oled_show_chinese(30,1,1,SHOW_SET_PWD); oled_show_chinese(50,1,2,SHOW_SET_PWD); oled_show_chinese(70,1,3,SHOW_SET_PWD); oled_show_chinese(90,1,4,SHOW_SET_PWD); oled_show_char(110,1,':',16); } //请设定密码 void oled_show_changed(void) { oled_fill(0x00); oled_show_chinese(10,1,0,SHOW_PWD_CHANGED); oled_show_chinese(30,1,1,SHOW_PWD_CHANGED); oled_show_chinese(50,1,2,SHOW_PWD_CHANGED); oled_show_chinese(70,1,3,SHOW_PWD_CHANGED); oled_show_chinese(90,1,4,SHOW_PWD_CHANGED); oled_show_chinese(110,1,5,SHOW_PWD_CHANGED); }
#ifndef __OLED_H__ #define __OLED_H__ #include "sys.h" enum message{ SHOW_INPUT_PWD = 0, SHOW_PWD_RIGHT, SHOW_PWD_WRONG, SHOW_INPUT_OLD_PWD, SHOW_INPUT_NEW_PWD, SHOW_PWD_CHANGED, SHOW_SET_PWD }; #define OLED_I2C_SCL_CLK() __HAL_RCC_GPIOB_CLK_ENABLE(); #define OLED_I2C_SCL_PORT GPIOB #define OLED_I2C_SCL_PIN GPIO_PIN_8 #define OLED_I2C_SDA_CLK() __HAL_RCC_GPIOB_CLK_ENABLE(); #define OLED_I2C_SDA_PORT GPIOB #define OLED_I2C_SDA_PIN GPIO_PIN_9 #define OLED_SCL_RESET() HAL_GPIO_WritePin(OLED_I2C_SCL_PORT, OLED_I2C_SCL_PIN, GPIO_PIN_RESET) #define OLED_SCL_SET() HAL_GPIO_WritePin(OLED_I2C_SCL_PORT, OLED_I2C_SCL_PIN, GPIO_PIN_SET) #define OLED_SDA_RESET() HAL_GPIO_WritePin(OLED_I2C_SDA_PORT, OLED_I2C_SDA_PIN, GPIO_PIN_RESET) #define OLED_SDA_SET() HAL_GPIO_WritePin(OLED_I2C_SDA_PORT, OLED_I2C_SDA_PIN, GPIO_PIN_SET) void oled_init(void); void oled_write_cmd(uint8_t cmd); void oled_write_data(uint8_t data); void oled_fill(uint8_t data); void oled_show_char(uint8_t x, uint8_t y, uint8_t num, uint8_t size); void oled_show_string(uint8_t x, uint8_t y, char *p, uint8_t size); void oled_show_chinese(uint8_t x, uint8_t y, uint8_t N, uint8_t size); void oled_show_image(uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint8_t *bmp); void oled_show_input(void); void oled_show_right(void); void oled_show_wrong(void); void oled_show_old(void); void oled_show_new(void); void oled_show_set(void); void oled_show_changed(void); #endif

接下来实现密码相关的一些功能,比如说读取密码 保存密码 密码比对 检查密码是否存在等

#include "password.h" #include "w25q128.h" #include "oled.h" #include "keyboard.h" #include "string.h" #include "stdio.h" #include "lock.h" #include "beep.h" #include "delay.h" #define PASSWORD_SIZE 10 /*对尺寸宏定义*/ uint8_t pwd_input[PASSWORD_SIZE] = {0}; /*定义的输入全局变量*/ uint8_t pwd_read[PASSWORD_SIZE] = {0}; /*读出来的全局变量*/ uint8_t i = 0, key_now = 0, try_times = 0; /*password数组索引、按键的地址、 //初始化函数 void password_init(void) { w25q128_init(); /*因为密码是存到w25q128中,初始化他就行*/ } / void password_input_clear(void) /*清空输入缓存*/ { memset(pwd_input, 0, PASSWORD_SIZE); /*清空的对象、变成0、长度尺寸、string.h*/ i = 0; /*password是个数组,有索引,也清0*/ } //保存密码 void password_save(void) /*为什么保存密码不需要传入参数*/ { /*将获取键盘输入的内容放到一个全局变量中放到w25q128*/ w25q128_erase_sector(0x000000); /*先擦除*/ w25q128_write_page(0x000000, pwd_input, PASSWORD_SIZE); /*在哪写、内容、尺寸*/ oled_show_changed(); /*密码已经修改*/ /*加入oled.h*/ } //获取键盘输入 /*输入进来的是全局变量,就不用传参数。返回值记录了最后按下的值是#还是星*/ uint8_t password_get_input(void) { password_input_clear(); /*清空*/ while(1) { /*keyboard.h*/ key_now = keyboard_get_value(); /*获取keyboard_get_value的返回值*/ if(key_now == POUND_KEY) /*井号宏*/ { printf("按下了井号键,input: %s\r\n", pwd_input); return POUND_KEY; } else if(key_now == STAR_KEY) /*星号宏*/ { printf("按下了星号键\r\n"); return STAR_KEY; } else if(key_now != 0) //因为key_now本身等于0,按0也不用担心 返回的是‘0’ { printf("按下了:%c\r\n", key_now); /*一个字符一个字符按 按下的打印出来*/ oled_show_char(20 + i * 10, 4, key_now, 16);/*屏幕上显示出来,高度16宽度8*/ pwd_input[i++] = key_now; /*保存完+1*/ /*列坐标、page、内容、尺寸*/ } } } //密码比对 uint8_t password_compare(void) /*比对要返回一个结果,用输入 比上 读取存起来的*/ { uint8_t i = 0; w25q128_read_data(0x000000, pwd_read, PASSWORD_SIZE); /*读出保存的密码*/ //strlen输入类型要求char* if(strlen((char *)pwd_input) != strlen((char *)pwd_read)) /*如果两种长度不同 错误*/ return FALSE; for(i = 0; i < strlen((char *)pwd_read); i++) /*如果长度相同,每一位依次读都一样true*/ { if(pwd_input[i] != pwd_read[i]) return FALSE; } return TRUE; } //密码输入正确的操作 void password_input_right_action(void) { oled_show_right(); /*屏幕先显示密码正确*/ lock_on(); /*lock.h*/ beep_on(); delay_ms(300); beep_off(); delay_ms(1000); lock_off(); try_times = 0; /*尝试次数清0*/ } //密码输入错误的操作 void password_input_wrong_action(void) { oled_show_wrong(); /*屏幕显示输入错误*/ try_times++; /*尝试次数+1*/ if(try_times >= 3) { beep_on(); delay_ms(1000); beep_off(); try_times = 0; } delay_ms(1000); } //旧密码输入正确的操作 void password_old_right_action(void) { oled_show_new(); /*屏幕显示 输入新密码*/ password_get_input(); /*先获取键盘输入*/ password_save(); /*保存密码到flash*/ beep_on(); delay_ms(300); beep_off(); delay_ms(500); } //旧密码输入错误的操作 void password_old_wrong_action(void) { oled_show_wrong(); /*屏幕显示 输入错误*/ delay_ms(1000); } //检查密码是否存在 void password_check(void) { w25q128_read_data(0x000000, pwd_read, PASSWORD_SIZE); /*读flash中 有没有保存数据*/ printf("读出密码:%s\r\n", pwd_read); if(pwd_read[0] == '\0' || pwd_read[0] == 0xFF)/*flash中默认是1*/ pwd_read默认是0=‘\0’ { oled_show_set(); /*清设置新密码*/ password_get_input(); /*获取用户输入*/ password_save(); } }
#ifndef __PASSWORD_H__ #define __PASSWORD_H__ #include "sys.h" #define POUND_KEY '#' #define STAR_KEY '*' #define TRUE 1 #define FALSE 0 void password_init(void); uint8_t password_get_input(void); uint8_t password_compare(void); void password_input_right_action(void); void password_input_wrong_action(void); void password_old_right_action(void); void password_old_wrong_action(void); void password_check(void); #endif

main函数的逻辑

#include "sys.h" #include "delay.h" #include "led.h" #include "uart1.h" #include "beep.h" #include "keyboard.h" #include "lock.h" #include "oled.h" #include "w25q128.h" #include "password.h" int main(void) { HAL_Init(); /* 初始化HAL库*/ stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */ uart1_init(115200); beep_init(); keyboard_init(); lock_init(); oled_init(); password_init(); // w25q128_init(); /*password初始化中包含了*/ printf("hello world!\r\n"); password_check(); /*先检查有没有密码 ,没有就设置*/ uint8_t key_last = 0; /*获取用户输入进来的密码*/ while(1) { oled_show_input(); /*OLED请输入密码*/ key_last = password_get_input(); /*一直获取键盘输入*/ if(key_last == POUND_KEY) { if(password_compare() == TRUE) password_input_right_action(); else password_input_wrong_action(); } else if(key_last == STAR_KEY) { oled_show_old(); /*输入旧密码*/ password_get_input(); /*获取输入*/ if(password_compare() == TRUE) /*对比正确*/ password_old_right_action(); else password_old_wrong_action(); } } }

烧录之后,现象就自己去尝试吧。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询