资讯动态

十二、SPI+(W25Q128+矩阵键盘)模块

发布时间:2026/9/12 19:19:33 来源:尧图企业网站定制
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, CS2根SCL, SDA通信方式全双工同时收发半双工分时收发速度快几十 MHz慢100k~400kHz最高3.4M寻址方式片选线CS选中从机7位/10位地址寻址从机数量每增加一个从机需要多一根CS线用地址区分数量受地址限制硬件复杂度线多稍复杂线少简单功耗较高多根线较低典型应用显示屏、SD卡、传感器、Flash存储器EEPROM、温湿度传感器、小屏幕2. SPI是什么SPI是串行外设接口Serial Peripheral Interface的缩写是一种高速的全双工同步串行的通信总线并且在芯片的管脚上只占用四根线节约了芯片的管脚同时为PCB的布局上节省空间提供方便正是出于这种简单易用的特性越来越多的芯片集成了这种通信协议比如AT91RM92003. 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为36MAPB2为72M的这么大的频率是没办法直接用的所以通过上面三位进行一些分频。这里主机移位寄存器是 低位先行那么如何设置呢 ---通过数据控制器。假设说stm32作为从机了那么此时MOSI就成为了输入端口此时就不是走直的路的而是走中间弯曲的路之后到达MISO。当移位寄存器的数据全部移完之后会产生一个TXE发送完成的信号说明发送缓冲区目前是一个空的状态就可以把数据写到发送缓冲区当移位寄存器空了之后就把TDR中的数据传进去。对应有个RDR当RDR接收完移位寄存器发送的数据后就会产生一个RXNE的信号。此时就可以把缓冲区数据读出来了。通过这两个flag进行配合就可以实现整个数据的收发。5. SPI工作模式之前说 来一次时钟就位移一位那么这个时钟到底是上升沿触发还是下降沿触发。时钟极性CPOL没有数据传输时时钟线处于空闲状态时的电平0SCK在空闲状态保持低电平即时钟线在没有数据传输时始终为低电平此时数据在时钟的上升沿被采样1SCK在空闲状态保持高电平即时钟线在没有数据传输时始终为高电平此时数据在时钟的下降沿被采样。时钟相位CPHA时钟线在第几个时钟边沿采样数据0SCK的第一奇数边沿进行数据位采样数据在第一个时钟边沿被锁存1SCK的第二偶数边沿进行数据位采样数据在第二个时钟边沿被锁存模式 0 和模式 3 最常用。模式 0 时序图模式 3 时序图6. SPI寄存器及库函数介绍位15双向数据模式使能全双工、位14决定只收只读模式和位15配合位11数据帧的格式8/16IIC和串口一般是配置为8位SPI看需求选8还是16位。位9软件从设备管理控制NSS上的引脚电平。一般随便拿个GPIO口去做NSS信号一般启动1位7帧格式决定是高位先行还是低位先行。位6SPI使能。位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.3CS DI DO CLK对应SPI的四根线。W25Q128存储架构W25Q128 将16M - byte的容量分为 256 个块block每块 64K 字节每块分为 16 个扇区sector一扇区 4K 字节每扇区分为 16 个页page一页 256 字节。W25Q128 的最小擦除单位为一个扇区也就是每次必须擦除 4K 个字节。这样我们需要给 W25Q128 开辟一个至少 4K 的缓存区。1M1024K 1K1024byte 10242的10次方16M16×2的10次方KB2的24次方 字节所以要访问2的24次方16777215个地址十六进制为0xFFFFFF用24位地址线。W25Q128常用指令W25Q128 有非常多的指令这里我们只介绍几个指令。指令HEX名称作用0x06写使能写入数据/擦除之前必须先发送该指令0x05读 SR1判定 FLASH 是否处于空闲状态擦除用0x03读数据读取数据0x02页写写入数据最多写256字节0x20扇区擦除扇区擦除指令最小擦除单位具体工作时序如下写使能(06H)执行页写扇区擦除块擦除片擦除写状态寄存器等指令前需要写使能。拉低 CS 片选 → 发送 06H → 拉高 CS 片选读SR105H拉低 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-LinkUSB转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, amp;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, amp;amp;gpio_initstruct); gpio_initstruct.Pin GPIO_PIN_6; /*MISO PA6*/ gpio_initstruct.Mode GPIO_MODE_INPUT; /*浮空输入*/ HAL_GPIO_Init(GPIOA, amp;amp;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) lt;lt; 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 lt; 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 lt; 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 /*三目运算符 xAAABBB*/ #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) /*x1 拉高电平否则拉低 。反斜杠 \ 是续行符*/ /* 指令表 */ #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, amp;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, amp;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 amp;amp; 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-LinkUSB转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);*/ /*fori0;i5;i*/ /* oled_show_chinese(10i*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); #endifmain函数的逻辑#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(); } } }烧录之后现象就自己去尝试吧。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价