做一名优秀网站设计师计划/优化seo报价
目录
SPI通信简介
硬件电路
移位示意图
SPI基本时序单元
SPI时序
W25Q64简介
硬件电路
W25Q64框图
Flash操作注意事项
SPI外设简介
SPI框图
SPI基本结构
主模式全双工连续传输
非连续传输
软件/硬件波形对比
SPI应用
软件SPI读写W25Q64
硬件SPI读写W25Q64
SPI通信简介
- SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
- 四根通信线:SCK(Serial Clock,串行时钟线)、MOSI(Master Output Slave Input,主机输出从机输入)、MISO(Master Input Slave Output,主机输入从机输出)、SS(Slave Select,从机选择)
- 同步,全双工
- 支持总线挂载多设备(一主多从)
SPI相对于I2C来说,SPI传输更快,SPI协议没有严格规定最大传输速度,最大传输速度取决于芯片设计厂商设计需求,比如W25Q64芯片手册里写的SPI时钟频率最大可达80MHz,比STM32主频还高,其次SPI的设计比较简单粗暴,功能没有I2C那么多学习起来比I2C简单,最后SPI的硬件开销比较大,通信线的个数比较多,并且通信过程中经常有资源浪费现象。
SCK有些地方也叫SCLK、CLK、CK。MOSI和MISO有的地方可能直接叫DO和DI。SS有的地方也叫NSS、CS。
SCK引脚提供时钟信号,数据位的输出和输入都是在SCK的上升沿或者下降沿进行。
SS从机选择线,有几个从机就开几条SS,需要哪个从机就控制接到对应从机的SS,给个低电平说明要找这个从机了。
硬件电路
- 所有SPI设备的SCK、MOSI、MISO分别连在一起
- 主机另外引出多条SS控制线,分别接到各从机的SS引脚
- 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
单端信号需要共地图上GND没画出来,但是是必须要接的。从机没有独立供电主机还要引出电源VCC给从机供电。
SCK时钟线完全由主机掌控,对主机来说时钟线为输出,对于所有从机来说时钟线都为输入,这样主机的同步时钟,就能送到各个从机了。
MOSI主机输出从机输入,数据传输方向是主机通过MOSI输出,所有从机通过MOSI 输入,MISO主机输入从机输出,数据传输方向是三个从机通过MISO输出,主机通过MISO输入。
主机的SS线都是输出,从机的SS线都是输入,SS线低电平有效,主机想指定谁就把对应SS输出置低电平就行了。同一时间主机只能置一个SS为低电平,只能选中一个从机,否则会数据冲突。
推挽输出高低电平都有较强驱动能力,这将使得SPI引脚信号的下降沿和上升沿都非常迅速,不像I2C那样下降沿非常迅速上升沿比较缓慢,得益于推挽输出的驱动能力SPI信号变化的快,那自然它就能达到更高的传输速度。
I2C并不是不想使用更快的推挽输出,而是I2C要实现半双工,经常要切换输入输出,而且I2C又要实现多主机的时钟同步和总线仲裁,这些功能都不允许I2C使用推挽输出要不然一不小心就电源短路了,所以I2C选择了更多的功能自然要放弃更强的性能了。
对于SPI来说首先不支持多主机,然后又是全双工,SPI的输出引脚始终是输出,输入引脚始终是输入,基本不会冲突,所以可以大胆使用推挽输出,不过SPI其实还是有一个冲突点,就是图上的MISO引脚,主机一个是输入,三个从机全都是输出,如果三个从机都始终是推挽输出那势必会导致冲突,所以SPI协议有一条规定:当从机的SS引脚为高电平也就是从机未被选中时它的MISO引脚必须切换为高阻态,高阻态就相当于引脚断开不输出任何电平,这样就可以防止一条线有多个输出而导致电平冲突的问题了,在SS为低电平时MISO才允许变为推挽输出,这个切换过程一般在从机里,我们一般都是写主机的程序,所以我们主机的程序不需要关注这个问题。
移位示意图
接下来演示电路如何工作:
首先我们规定波特率发生器时钟的上升沿所有移位寄存器向左移动一位,移出去的数据位放到引脚上,波特率发生器时钟的下降沿引脚上的位采样输入到移位寄存器的最低位。
接下来假设主机有个数据1010 1010要发送到从机,同时从机有个数据0101 0101要发送到主机,那我们就可以驱动时钟,先产生一个上升沿,这时所有的为向左移动一次,从最高位移出去的数据放到通信线上,实际就是放到了输出数据寄存器:
上图就是第一个时钟上升沿执行的结果,就是把主机和从机中移位寄存器的最高位分别放到MOSI和MISO的通信线上。
之后时钟继续运行,上升沿之后下一个边沿就是下降沿,下降沿时主机和从机内都会进行数据采样输入:
这就是第一个时钟结束后的现象。那时钟继续运行,下一个上升沿同样的操作移位输出:
随后下降沿数据采样输入:
最终8个时钟之后,主机原来的1010 1010跑到从机里了,原来从机里的0101 0101跑到主机里了,这就实现了主机和从机一个字节的数据交换
SPI的数据收发都是基于字节交换这个基本单元来进行的,当主机需要发送一个字节并且同时需要接收一个字节时就可以执行一下字节交换的时序,如果只想发送不想接收仍然调用交换字节的时序,发送同时接收,只是这个接收到的数据我们不看它就行了 ,如果只想接收不想发送还是调用交换字节的时序,发送同时接收,只是我们会随便发送一个数据,只要能把从机的数据置换过来就好了,我们读取置换过来的数据就是接收了,这里我们随便发过去的数据从机也不会看它,一般这个随便的数据是0x00或者0xFF。
SPI基本时序单元
- 起始条件:SS从高电平切换到低电平
- 终止条件:SS从低电平切换到高电平
接下来就是数据传输的基本单元了,这个基本单元建立在我们刚才说的移位模型上,并且什么时候开始移位,上升沿移位还是下降沿移位SPI并没有限定死,给了我们可以配置的选择,这样SPI就能兼容更多芯片。
SPI有两个可配置的位,分别是CPOL(Clock Polarity)时钟极性,CPHA(Clock Phase)时钟相位,每一位可以配置为1或0,总共组合起来就有模式0、模式1、模式2、模式3四种模式。
先看模式1,这个模式与前文提到的移位模型是对应的:
主机通过MOSI移出最高位,此时MOSI的电平就表示主机想要发送的数据的B7,从机通过MISO移出最高位,此时MISO表示从机想要发送的数据的B7,然后时钟运行产生下降沿,此时主机和从机同时移入数据也就是进行数据采样,这里主机移出的B7进入从机移位寄存器最低位,从机移出的B7进入主机移位寄存器最低位,这样一个时钟脉冲产生完毕,一个数据位传输完毕。接下来就是同样的过程,最后一个下降沿B0传输完成,自此主机和从机完成了一个字节的数据交换,如果主机只想交换一个字节那这时就可以置SS为高电平结束通信了,在SS上升沿MOSI还可以在变化一次,将MOSI置到一个默认高电平或者低电平,当然也可以不管它因为SPI没有硬性规定MOSI的默认电平,然后MISO从机必须得置回高阻态,此时如果主机的MISO为上拉输入那MISO引脚电平就是默认的高电平,如果主机MISO为浮空输入那MISO引脚电平不确定,如果主机还想继续交换字节,在此时主机就不必把SS置回高电平,直接重复一下交换字节的时序。
之后我们看一下模式0:
SS下降沿触发了输出,SCK上升沿就可以采样输入数据了,这样B7就传输完毕,之后SCK下降沿移出B6,SCK上升沿移入B6,最终在第8个上升沿B0位移入完成,整个字节交换完成,之后SCK还有个下降沿,如果主机只需要交换一个字节就结束那在整个下降沿MOSI可以置回默认电平或者不管它,MISO也会变化一次,这一位实际上是下一个字节的B7,因为这个相位提前了,所以下一个字节的B7会露个头,如果不需要的话SS上升沿之后从机MISO置回高阻态。如果主机想交换多个字节,那就继续调用时序,在最后一个下降沿主机放下一个字节的B7,从机也放下一个字节的B7,SCK上升沿正好接着采样第二个字节的B7。
模式0和模式1的区别就在于模式0把这个数据变化的时机提前了,在实际应用中模式0是最常用的。
模式2和模式3:
模式0和模式2的区别就是模式0的CPOL=0,模式2的CPOL=1,两者的波形就是SCK的极性取反一下,剩下的流程完全一致;模式1和模式3的区别,也是模式1的CPOL=0,模式3的CPOL=1,两者的波形也是SCK的极性取反一下其他地方没有变化。
模式0和模式3都是SCK上升沿采样,模式1和模式2都是SCK下降沿采样。
SPI时序
接下来看几个完整的SPI时序波形,以W25Q64为例。SPI对字节流功能的规定不像I2C,I2C规定一般是有效数据流第一个字节是寄存器地址,之后依次是读写的数据,使用的是读写寄存器的模型。而SPI通常采用指令码加读写数据的模型,这个过程就是SPI起始后,第一个交换发送给从机的数据一般叫指令码,在从机中对应会有一个指令集,当我们需要发送什么指令时,就可以在起始后第一个字节发送指令集里面的数据,这样就能指导从机完成相应的功能了,不同的指令可以有不同的数据个数。
整个时序的功能就是发送指令,指令码是0x06,从机一比对事先定义好的指令集发现0x06是写使能的指令,那从机就会控制硬件进行写使能。
地址24位,分3个字节传输,时序首先SS下降沿开始时序,第一个字节用0x02换来了0xFF,其中发送的0x02是一条指令代表这是一个写数据的时序,接收的0xFF不需要看。后面跟着写的地址和数据,第二个字节用0x12换来了0xFF,W25Q64规定写指令之后的字节定义为地址高位,所以这个0x12就表示地址的23~16位,之后同理,三个字节交换24位地址发送完毕,从机收到的24位地址是0x123456,之后发送写入的数据。
W25Q64简介
- W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景
- 存储介质:Nor Flash(闪存)
- 时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)
- 存储容量(24位地址):
W25Q40: 4Mbit / 512KByte
W25Q80: 8Mbit / 1MByte
W25Q16: 16Mbit / 2MByte
W25Q32: 32Mbit / 4MByte
W25Q64: 64Mbit / 8MByte
W25Q128: 128Mbit / 16MByte
W25Q256: 256Mbit / 32MByte
硬件电路
W25Q64框图
数据从缓冲区到Flash里需要一定时间,所以在写入时序结束后芯片会进入一段忙的状态,会有一条线通往状态寄存器的BUSY位,将该位置1,忙的时候芯片就不会响应新的读写时序了。
Flash操作注意事项
写入操作时:
- 1)写入操作前,必须先进行写使能
- 2)每个数据位只能由1改写为0,不能由0改写为1
- 3)写入数据前必须先擦除,擦除后,所有数据位变为1
- 4)擦除必须按最小擦除单元进行
- 5)连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
- 6)写入操作结束后,芯片进入忙状态,不响应新的读写操作
- 读取操作时:直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取
SPI外设简介
- STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担
- 可配置8位/16位数据帧、高位先行/低位先行
- 时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)
- 支持多主机模型、主或从操作
- 可精简为半双工/单工通信
- 支持DMA
- 兼容I2S协议
- STM32F103C8T6 硬件SPI资源:SPI1、SPI2
我们SPI最常用的配置是8位数据帧高位先行。I2S是数字音频信号传输的协议,和I2C区别很大不要搞混了,了解。
SPI框图
发送数据先写入TDR,再转到移位寄存器发送,发送的同时接收数据,接收到的数据转到RDR,再从RDR读取数据。
SPI基本结构
主模式全双工连续传输
非连续传输
主模式全双工连续传输比较复杂,也不太方便封装,在实际过程中如果对性能没有极致的追求,我们更倾向于使用这个非连续传输的,非连续传输更加简单实际用的话只需要4行代码就能完成任务。
按照这个,我们的流程就是,第一步等待TXE为1,第二步写入发送的数据到TDR,第三步等待RXNE为1,第四步读取RDR接收到的数据,之后交换第二个字节重复这四步。
非连续传输缺点就是没有及时把下一个数据写入TDR等着,所以等待上一个字节时序完成后下一个字节还没有来,那这个数据传输就会在这里等,会拖慢数据传输速度,这个间隙在SCK频率低的时候影响不大,SCK频率非常高时影响就比较大。
软件/硬件波形对比
SPI应用
软件SPI读写W25Q64
软件模拟的SPI的CS、DO、CLK、DI线可以接到STM32的任意GPIO口。
程序框架如下:
首先建立MySPI模块,在MySPI_Init函数里初始化通信引脚,接线图中我们使用PA4、5、6、7引脚,开启GPIOA时钟,前文提到输出引脚配置为推挽输出,输入引脚配置为浮空输入或者上拉输入,对于主机来说时钟、主机输出和片选都是输出引脚,所以这3个脚配置为推挽输出,剩下一个主机输入是输入引脚我们选择上拉输入。
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
接下来把置引脚高低电平的函数都封装换个名字:
/*
* 写SS的引脚
*/
void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}/*
* 写SCK的引脚
*/
void MySPI_W_SCK(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}/*
* 写MISO的引脚
*/
void MySPI_W_MOSI(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}/*
* 读MISO的引脚
*/
uint8_t MySPI_R_MISO(void)
{return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}
MySPI_Init函数里我们还有一个工作要做就是置一下初始化之后引脚的默认电平,SS高电平不选中从机,SCK我们计划使用SPI模式0所以默认低电平:
/* 置默认电平 */
MySPI_W_SS(1);//SS默认高电平,不选中从机
MySPI_W_SCK(0);//计划用SPI模式0,所以默认低电平
接下来我们开始写SPI的三个时序基本单元:
/*起始信号
* 直接把SS置低电平
*/
void MySPI_Start(void)
{MySPI_W_SS(0);
}/*终止信号
* 直接把SS置高电平
*/
void MySPI_Stop(void)
{MySPI_W_SS(1);
}/*交换一个字节(模式0)
* 参数 ByteSend:通过交换一个字节发送出去的数据
* 返回值:通过交换一个字节接收到的数据
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i,ByteReceive = 0x00;//用于接收for(i = 0;i < 8;i++){MySPI_W_MOSI(ByteSend & (0x80 >> i));//初始SCK低电平,主机移出一位数据到MOSI(调用写MOSI),从机移出一位到MISO(从机的事不归我们管)MySPI_W_SCK(1);//SCK产生上升沿,主机和从机同时移入数据,从机自动把MOSI的数据读走(不归我们管),主机读取MISO的数据(上升沿后进行)if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}//主机把从机刚才放在MISO的数据位读进来MySPI_W_SCK(0);//SCK产生下降沿,主机从机移出下一位数据}return ByteReceive;
}
接下来按照计划写下一个模块,在SPI通信层基础上,建立W25Q64驱动层。同理先在W25Q64_Init函数进行初始化, 由于不需要初始化其他东西所以只调用SPI_Init即可。
void W25Q64_Init(void)
{MySPI_Init();}
现在我们来实现业务代码也就是拼接完整时序,这个我们要参考手册,主要参考指令集表格:
每个指令对应一个指令码,直接写数字意义不太明显,可读性不高,所以我们单独建立一个头文件W26Q64_Ins.s存放宏定义:
#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H#define W25Q64_WRITE_ENABLE 0x06
#define W25Q64_WRITE_DISABLE 0x04
#define W25Q64_READ_STATUS_REGISTER_1 0x05
#define W25Q64_READ_STATUS_REGISTER_2 0x35
#define W25Q64_WRITE_STATUS_REGISTER 0x01
#define W25Q64_PAGE_PROGRAM 0x02
#define W25Q64_QUAD_PAGE_PROGRAM 0x32
#define W25Q64_BLOCK_ERASE_64KB 0xD8
#define W25Q64_BLOCK_ERASE_32KB 0x52
#define W25Q64_SECTOR_ERASE_4KB 0x20
#define W25Q64_CHIP_ERASE 0xC7
#define W25Q64_ERASE_SUSPEND 0x75
#define W25Q64_ERASE_RESUME 0x7A
#define W25Q64_POWER_DOWN 0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE 0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID 0x90
#define W25Q64_READ_UNIQUE_ID 0x4B
#define W25Q64_JEDEC_ID 0x9F
#define W25Q64_READ_DATA 0x03
#define W25Q64_FAST_READ 0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B
#define W25Q64_FAST_READ_DUAL_IO 0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B
#define W25Q64_FAST_READ_QUAD_IO 0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3#define W25Q64_DUMMY_BYTE 0xFF#endif
前文提到写操作前必须进行写使能,涉及写使能1的时序有扇区擦除和页编程,为了方便使用我们直接在函数里自带写使能。还有一个就是写入操作后芯片进入忙状态,所以我们在每次写操作结束后调用一下等待BUSY清零的函数。
/*读取8位厂商ID(MID)和16位设备ID(DID)
*/
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{MySPI_Start();MySPI_SwapByte(W25Q64_JEDEC_ID);//参数是交换发送的0x9F(读ID号的指令),返回值是交换接收的但是这里没有意义,返回值就不要了*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//将从机发送的ID号置换过来,随便发一个0xFF,这个返回值是我们要的MID*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//这个返回值是我们要的DID高8位*DID <<= 8;*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);//这个返回值是我们要的DID低8位MySPI_Stop();
}/*写使能
*/
void W25Q64_WriteEnable(void)
{MySPI_Start();MySPI_SwapByte(W25Q64_WRITE_ENABLE);MySPI_Stop();
}/*等待忙函数,读状态寄存器1,一般读最低位BUSY判断是不是忙状态,1表示芯片还在忙
*/
void W25Q64_WaitBusy(void)
{uint32_t TimeOut;MySPI_Start();MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);TimeOut = 100000;while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)//等待BUSY为0{TimeOut --;if(TimeOut == 0)//超时退出{break;}};MySPI_Stop();
}/*页编程
*/
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{uint16_t i;W25Q64_WriteEnable();//写使能MySPI_Start();MySPI_SwapByte(W25Q64_PAGE_PROGRAM);//交换发送3个字节的地址,例如0x123456,>>16就是0x12,>>8就是0x1234舍弃高位就是0x34,不移动舍弃高位就是0x56MySPI_SwapByte(Address >> 16);MySPI_SwapByte(Address >> 8);MySPI_SwapByte(Address);//写入for(i = 0;i < Count;i++){MySPI_SwapByte(DataArray[i]);}MySPI_Stop();W25Q64_WaitBusy();//事后等待
}/*擦除(扇区擦除)
*/
void W25Q64_SectorErase(uint32_t Address)
{W25Q64_WriteEnable();//写使能MySPI_Start();MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);//指定地址MySPI_SwapByte(Address >> 16);MySPI_SwapByte(Address >> 8);MySPI_SwapByte(Address);MySPI_Stop();W25Q64_WaitBusy();//事后等待
}/*读取数据
*/
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{uint32_t i;MySPI_Start();MySPI_SwapByte(W25Q64_READ_DATA);//指定地址MySPI_SwapByte(Address >> 16);MySPI_SwapByte(Address >> 8);MySPI_SwapByte(Address);//接收for(i = 0;i < Count;i++){DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);}MySPI_Stop();
}
完整代码:
MySPI.c:
#include "stm32f10x.h" // Device header/*
* 写SS的引脚
*/
void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}/*
* 写SCK的引脚
*/
void MySPI_W_SCK(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}/*
* 写MISO的引脚
*/
void MySPI_W_MOSI(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}/*
* 读MISO的引脚
*/
uint8_t MySPI_R_MISO(void)
{return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}void MySPI_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);/* 置默认电平 */MySPI_W_SS(1);//SS默认高电平,不选中从机MySPI_W_SCK(0);//计划用SPI模式0,所以默认低电平}/*起始信号
* 直接把SS置低电平
*/
void MySPI_Start(void)
{MySPI_W_SS(0);
}/*终止信号
* 直接把SS置高电平
*/
void MySPI_Stop(void)
{MySPI_W_SS(1);
}/*交换一个字节(模式0)
* 参数 ByteSend:通过交换一个字节发送出去的数据
* 返回值:通过交换一个字节接收到的数据
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i,ByteReceive = 0x00;//用于接收for(i = 0;i < 8;i++){MySPI_W_MOSI(ByteSend & (0x80 >> i));//初始SCK低电平,主机移出一位数据到MOSI(调用写MOSI),从机移出一位到MISO(从机的事不归我们管)MySPI_W_SCK(1);//SCK产生上升沿,主机和从机同时移入数据,从机自动把MOSI的数据读走(不归我们管),主机读取MISO的数据(上升沿后进行)if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}//主机把从机刚才放在MISO的数据位读进来MySPI_W_SCK(0);//SCK产生下降沿,主机从机移出下一位数据}return ByteReceive;
}
W25Q64.c:
#include "stm32f10x.h" // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h" void W25Q64_Init(void)
{MySPI_Init();}/*读取8位厂商ID(MID)和16位设备ID(DID)
*/
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{MySPI_Start();MySPI_SwapByte(W25Q64_JEDEC_ID);//参数是交换发送的0x9F(读ID号的指令),返回值是交换接收的但是这里没有意义,返回值就不要了*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//将从机发送的ID号置换过来,随便发一个0xFF,这个返回值是我们要的MID*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//这个返回值是我们要的DID高8位*DID <<= 8;*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);//这个返回值是我们要的DID低8位MySPI_Stop();
}/*写使能
*/
void W25Q64_WriteEnable(void)
{MySPI_Start();MySPI_SwapByte(W25Q64_WRITE_ENABLE);MySPI_Stop();
}/*等待忙函数,读状态寄存器1,一般读最低位BUSY判断是不是忙状态,1表示芯片还在忙
*/
void W25Q64_WaitBusy(void)
{uint32_t TimeOut;MySPI_Start();MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);TimeOut = 100000;while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)//等待BUSY为0{TimeOut --;if(TimeOut == 0)//超时退出{break;}};MySPI_Stop();
}/*页编程
*/
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{uint16_t i;W25Q64_WriteEnable();//写使能MySPI_Start();MySPI_SwapByte(W25Q64_PAGE_PROGRAM);//交换发送3个字节的地址,例如0x123456,>>16就是0x12,>>8就是0x1234舍弃高位就是0x34,不移动舍弃高位就是0x56MySPI_SwapByte(Address >> 16);MySPI_SwapByte(Address >> 8);MySPI_SwapByte(Address);//写入for(i = 0;i < Count;i++){MySPI_SwapByte(DataArray[i]);}MySPI_Stop();W25Q64_WaitBusy();//事后等待
}/*擦除(扇区擦除)
*/
void W25Q64_SectorErase(uint32_t Address)
{W25Q64_WriteEnable();//写使能MySPI_Start();MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);//指定地址MySPI_SwapByte(Address >> 16);MySPI_SwapByte(Address >> 8);MySPI_SwapByte(Address);MySPI_Stop();W25Q64_WaitBusy();//事后等待
}/*读取数据
*/
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{uint32_t i;MySPI_Start();MySPI_SwapByte(W25Q64_READ_DATA);//指定地址MySPI_SwapByte(Address >> 16);MySPI_SwapByte(Address >> 8);MySPI_SwapByte(Address);//接收for(i = 0;i < Count;i++){DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);}MySPI_Stop();
}
main.c:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"uint8_t MID;
uint16_t DID;uint8_t ArrayWrite[] = {0x01,0x02,0x03,0x04};
uint8_t ArrayRead[4];uint8_t Key_Num;int main(void)
{OLED_Init();W25Q64_Init();OLED_ShowString(1, 1, "MID: DID:");OLED_ShowString(2, 1, "W:");OLED_ShowString(3, 1, "R:");W25Q64_ReadID(&MID, &DID);OLED_ShowHexNum(1, 5, MID, 2);OLED_ShowHexNum(1, 12, DID, 4);W25Q64_SectorErase(0x000000);//擦除W25Q64_PageProgram(0x000000, ArrayWrite, 4);//写入W25Q64_ReadData(0x000000, ArrayRead, 4);//读取OLED_ShowHexNum(2, 3, ArrayWrite[0],2);OLED_ShowHexNum(2, 6, ArrayWrite[1],2);OLED_ShowHexNum(2, 9, ArrayWrite[2],2);OLED_ShowHexNum(2, 12, ArrayWrite[3],2);OLED_ShowHexNum(3, 3, ArrayRead[0],2);OLED_ShowHexNum(3, 6, ArrayRead[1],2);OLED_ShowHexNum(3, 9, ArrayRead[2],2);OLED_ShowHexNum(3, 12, ArrayRead[3],2);while(1){}
}
硬件SPI读写W25Q64
硬件SPI引脚就不能任意选择了:
NSS我们继续使用软件模拟的方式实现,NSS没必要必须接在PA4,其他三个引脚就必须是PA5、6、7了。我们计划用SPI1,所以SCK接PA5,MISO接PA6,MOIS接PA7。
在上一个代码的MySPI底层修改即可,先看一下SPI相关库函数:
void SPI_I2S_DeInit(SPI_TypeDef* SPIx);//恢复缺省配置
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);//初始化
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);//结构体初始化
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);//外设使能
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);//中断使能
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);//DMA使能
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);//写DR数据寄存器,写TDR
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);//读DR数据寄存器,返回RDR的值
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);//获取标志位
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);//清除标志位
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);//获取中断标志位
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);//清除中断标志位
第一步开启SPI和GPIO时钟。
/* 第一步初始化GPIO与SPI时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
第二步,第二步初始化GPIO口。
/* 第二步初始化GPIO口,SCK和MOSI是硬件外设控制的输出信号配置为复用推挽输出,MISO是硬件外设输入信号配置为上拉输入 */
/* SS是软件控制的输出信号配置为通用推挽输出 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//通用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
第三步配置SPI外设。SPI_Init第一个参数SPI1,第二个参数结构体:
- SPI_Mode,SPI模式,决定当前设备是SPI的主机还是从机
- SPI_Direction,配置SPI裁剪引脚
- SPI_DataSize,配置8位还是16位数据帧
- SPI_FirstBit,配置高位先行还是低位先行
- SPI_BaudRatePrescaler,波特率预分频器配置SCK时钟频率
- SPI_CPOL,时钟极性
- SPI_CPHA,时钟相位,第几个边沿采样(移入)
- SPI_NSS,NSS引脚,
- SPI_CRCPolynomial,CRC校验多项式
/* 第三步配置SPI外设 */
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;//SPI模式,决定当前设备是SPI的主机还是从机,我们选主机
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//配置SPI裁剪引脚,我们用标准模式双线全双工
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;//配置8位还是16位数据帧,我们选8位
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//配置高位先行还是低位先行,我们选高位先行
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;//波特率预分频器配置SCK时钟频率
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;//时钟极性,我们用SPI模式0也就是默认低电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;//时钟相位,第几个边沿采样(移入),我们用SPI模式0所以我们选第一个开始采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//NSS引脚我们计划使用GPIO模拟,外设的NSS我们不用,选软件NSS就行了
SPI_InitStructure.SPI_CRCPolynomial = 7;//CRC校验多项式,我们不用,填默认值7
SPI_Init(SPI1, &SPI_InitStructure);
第四步开关控制SPI使能。
/* 第四步开关控制SPI使能 */
SPI_Cmd(SPI1, ENABLE);
最后默认SS输出高电平,不选中从机。
MySPI_W_SS(0);//默认SS输出高电平,不选中从机
这样SPI初始化函数就写好了,SPI外设就绪,我们就可以完成交换字节的函数了。调用交换字节函数,硬件SPI外设就要自动控制SCK、MOSI、MISO引脚生成时序。根据前文提到的四步。
/*起始信号
* 把SS置低电平
*/
void MySPI_Start(void)
{MySPI_W_SS(0);
}/*终止信号
* 把SS置高电平
*/
void MySPI_Stop(void)
{MySPI_W_SS(1);
}/*交换一个字节(模式0)
* 参数 ByteSend:通过交换一个字节发送出去的数据
* 返回值:通过交换一个字节接收到的数据
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);//等待TXE为1SPI_I2S_SendData(SPI1, ByteSend);//软件写入数据到SPI_DR,ByteSend写入到TDR,//之后ByteSend自动转入移位寄存器一旦移位寄存器有数据时序波形就会自动产生while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);//等待RXNE为1return SPI_I2S_ReceiveData(SPI1);//读取RDR接收的数据,就是置换接收的一个字节
}
完整代码:
MySPI.c:
#include "stm32f10x.h" // Device header/*
* 写SS的引脚
*/
void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}void MySPI_Init(void)
{/* 第一步初始化GPIO与SPI时钟 */RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);/* 第二步初始化GPIO口,SCK和MOSI是硬件外设控制的输出信号配置为复用推挽输出,MISO是硬件外设输入信号配置为上拉输入 *//* SS是软件控制的输出信号配置为通用推挽输出 */GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//通用推挽输出GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);/* 第三步配置SPI外设 */SPI_InitTypeDef SPI_InitStructure;SPI_InitStructure.SPI_Mode = SPI_Mode_Master;//SPI模式,决定当前设备是SPI的主机还是从机,我们选主机SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//配置SPI裁剪引脚,我们用标准模式双线全双工SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;//配置8位还是16位数据帧,我们选8位SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//配置高位先行还是低位先行,我们选高位先行SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;//波特率预分频器配置SCK时钟频率SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;//时钟极性,我们用SPI模式0也就是默认低电平SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;//时钟相位,第几个边沿采样(移入),我们用SPI模式0所以我们选第一个开始采样SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//NSS引脚我们计划使用GPIO模拟,外设的NSS我们不用,选软件NSS就行了SPI_InitStructure.SPI_CRCPolynomial = 7;//CRC校验多项式,我们不用,填默认值7SPI_Init(SPI1, &SPI_InitStructure);/* 第四步开关控制SPI使能 */SPI_Cmd(SPI1, ENABLE);MySPI_W_SS(0);//默认SS输出高电平,不选中从机}/*起始信号
* 把SS置低电平
*/
void MySPI_Start(void)
{MySPI_W_SS(0);
}/*终止信号
* 把SS置高电平
*/
void MySPI_Stop(void)
{MySPI_W_SS(1);
}/*交换一个字节(模式0)
* 参数 ByteSend:通过交换一个字节发送出去的数据
* 返回值:通过交换一个字节接收到的数据
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);//等待TXE为1SPI_I2S_SendData(SPI1, ByteSend);//软件写入数据到SPI_DR,ByteSend写入到TDR,//之后ByteSend自动转入移位寄存器一旦移位寄存器有数据时序波形就会自动产生while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);//等待RXNE为1return SPI_I2S_ReceiveData(SPI1);//读取RDR接收的数据,就是置换接收的一个字节
}
相关文章:

STM32学习笔记09-SPI通信
目录 SPI通信简介 硬件电路 移位示意图 SPI基本时序单元 SPI时序 W25Q64简介 硬件电路 W25Q64框图 Flash操作注意事项 SPI外设简介 SPI框图 SPI基本结构 主模式全双工连续传输 非连续传输 软件/硬件波形对比 SPI应用 软件SPI读写W25Q64 硬件SPI读写W25Q64 SP…...

树------二叉树
什么是树: 树是一种特殊的结构,由多个节点连接构成,并且不包含回路,也可以认为树是不包含回路的无向连通图,具体如下图所示。 当我们要确定一棵树的形态时,要指定一个根节点,没有父亲节点的节点…...

如何对加密后的数据进行模糊查询(面试题)
目录 前言1. 基本知识2. 国内做法 前言 这道题在面试比较常见,但是在算法逻辑层面中,直接对加密数据进行模糊查询是不可行的,因为加密算法会使数据变成不可读的形式 需要在加密过程中采取特殊的策略来支持模糊查询 以下只是结合网上现有的资…...

【MYSQL】当前读和快照读
前言 复习下隔离级别: 1、读未提交:一个事务还没提交时,它做的变更就能被别的事务看到。 2、读提交:一个事务提交之后,它做的变更会被其他事务看到 3、可重复读:一个事务执行过程中看到的数据,…...

C语言-使用数组法,指针法实现将一个5X5的矩阵中最大的元素放在中心,四个角分别放四个最小的元素(顺序为从左到右,从上到下,从小到大存放),写一函数实现之。
1.题目要求: 将一个5X5的矩阵中最大的元素放在中心,四个角分别放四个最小的元素(顺序为从左到右,从上到下,从小到大存放),写一函数实现之。 2.数组法实现 #define _CRT_SECURE_NO_WARNINGS 1…...

Android gradle 构建
Understanding Tasks - Gradle task kapt 是 Kotlin 语言的注解处理器,它是 Android Studio 中用于处理 Kotlin 注解的工具。它通过在编译期间生成代码来增强 Kotlin 代码的功能。需要 Kotlin 编译器来解析和处理注解;使用 APT 来生成代码,…...

vulnhub系列:devguru
vulnhub系列:devguru 靶机下载 一、信息收集 nmap扫描存活,根据mac地址寻找IP nmap 192.168.23.0/24nmap扫描端口,开放端口:22、80、8585 nmap 192.168.23.147 -p- -sV -Pn -O访问80端口 dirb目录扫描,存在 git 源…...

Robot Operating System——高质量图像传输
大纲 应用场景定义字段解释 案例 sensor_msgs::msg::Image 是 ROS (Robot Operating System) 中的一个消息类型,用于表示未压缩的图像数据。它通常用于传输和处理高质量的图像数据。 应用场景 机器人视觉 图像处理:在机器人视觉系统中,未压缩…...

NLP_情感分类_预训练加微调方案
文章目录 项目背景代码导包一些模型以及训练的参数设置定义dataset定义模型读取数据声明训练及测试数据集将定义模型实例化打印模型结构模型训练测试集效果 同类型项目 项目背景 项目的目的,是为了对情感评论数据集进行预测打标。在训练之前,需要对数据…...

全网最适合入门的面向对象编程教程:36 Python的内置数据类型-字典
全网最适合入门的面向对象编程教程:36 Python 的内置数据类型-字典 摘要: 字典是非常好用的容器,它可以用来直接将一个对象映射到另一个对象。一个拥有属性的空对象在某种程度上说就是一个字典,属性名映射到属性值。在内部&#…...

DataWind看板绘制案例
摘要: 1. 在不清楚DataWind看板怎么画的情况,可以先把表格给实现了,然后找几个有价值的数据进行看板实现 2. 还是不知道怎么画的情况,就去模仿其他人的案例; 3. 多看看DataWind提供的函数用法,就可以把表达式的使用运用起来了; 飞书官方文档:https://www.volcen…...

Golang | Leetcode Golang题解之第335题路径交叉
题目: 题解: func isSelfCrossing(distance []int) bool {n : len(distance)// 处理第 1 种情况i : 0for i < n && (i < 2 || distance[i] > distance[i-2]) {i}if i n {return false}// 处理第 j 次移动的情况if i 3 && di…...

C# 在Word中插入或删除分节符
在Word中,分节符是一种强大的工具,用于将文档分成不同的部分,每个部分可以有独立的页面设置,如页边距、纸张方向、页眉和页脚等。正确使用分节符可以极大地提升文档的组织性和专业性,特别是在长文档中,需要…...

基于STM32+Qt设计的无人超市收银系统(206)
文章目录 一、前言1.1 项目介绍【1】项目功能介绍【2】设计实现的功能【3】项目硬件模块组成1.2 设计思路【1】整体设计思路【2】上位机设计思路1.3 项目开发背景【1】选题的意义【2】可行性分析【3】参考文献【4】摘要【5】国内外技术发展现状1.4 开发工具的选择【1】设备端开…...

开源免费的表单收集系统TDuck
TDuck(填鸭表单)是一款开源免费的表单收集系统,它基于Apache 2.0协议开源,用户可以随时下载源码,自由修改和定制,也可以参与到项目的贡献和反馈中。TDuck表单系统不仅支持私有化部署,还提供了丰…...

Python 生成器、迭代器、可迭代对象 以及应用场景
Python 生成器(Generators) 生成器是一种特殊的迭代器,它使用 yield 语句来逐次产生数据,而不是一次性在内存中生成数据。这意呀着生成器提供了一种懒加载(lazy evaluation)的方式,非常适合处理…...

马斯克对欧盟的反应
每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领…...

uniapp + 安卓APP + H5 + 微信小程序实现PDF文件的预览和下载
文章目录 uniapp 安卓APP H5 微信小程序实现PDF文件的预览和下载1、用到的技术及插件2、简述操作:下载预览 3、上代码:(主要是写后端,前端不大熟,我感觉写的还凑活,不对的请指正嘻嘻)4、注意的问题 uniapp 安卓APP…...

Elasticsearch 8 RAG 技术分享
作者:来自 Elastic 中国区首席架构师 Jerry 本文由 Elastic 中国区首席架构师 Jerry Zhu 在【AI 搜索 TechDay】上的分享整理而成。【AI 搜索 TechDay】 是 Elastic 和阿里云联合主办的 AI 技术 Meetup 系列,聚焦企业级 AI 搜索应用和开发者动手实践&am…...

根据字典值回显,有颜色的
背景 本项目以若依前端vue2版本为例,项目中有根据字典值回显文本的函数selectDictLabel,但是有时候我们需要带颜色的回显,大概这样的 用法 <template v-slotscope><dict-label :options"dangerLevelOptions" :value&qu…...

多台PC网络ADB连接同一台RK3399 Android7.1.2设备
在RK3399 Android7.1.2上面,进行网络ADB调试时,如果多台电脑连接同一台Android设备,第一台连接上的能正常操作,之后连接的看到设备状态为OFFLINE,分析了下ADBD相关代码,发现在ACCEPT Client的时候没有区分别…...

前端黑科技:使用 JavaScript 实现网页扫码功能
在数字化时代,二维码已经渗透到我们生活的方方面面。从移动支付到产品溯源,二维码凭借其便捷性和高效性,成为了信息传递的重要载体。而随着前端技术的不断发展,我们甚至可以使用 JavaScript 在网页端实现二维码扫描功能࿰…...

【人工智能】全景解析:【机器学习】【深度学习】从基础理论到应用前景的【深度探索】
目录 1. 人工智能的基本概念 1.1 人工智能的定义与发展 1.1.1 人工智能的定义 1.1.2 人工智能的发展历史 1.2 人工智能的分类 1.2.1 弱人工智能 1.2.2 强人工智能 1.2.3 超人工智能 1.3 人工智能的关键组成部分 1.3.1 数据 1.3.2 算法 1.3.3 计算能力 2. 机器学习…...

MySQL与PostgreSQL语法区别
1. 数据类型差异 a. 整型 ● MySQL中的text数据类型最大存储容量为64KB,PostgreSQL中的text类型没有此限制。 ● MySQL中使用tinyint、mediumint和int表示不同大小的整数,PostgreSQL使用smallint、int和bigint。 b. 浮点数类型 ● MySQL提供了float和…...

vue2+OpenLayers 天地图上凸显出当前地理位置区域(4)
凸显出当前区域 需要当前地方的json数据 这个可以在阿里的这个阿里 看下效果图 遮盖层的逃命都是可以调的 引入 下面一段代码 import sx from "/views/json/sx1.json"; // 下载的json import GeoJSON from "ol/format/GeoJSON"; // ol的一些方法 imp…...

基于Python、Django开发Web计算器
1、创建项目 创建Django项目参照https://blog.csdn.net/qq_42148307/article/details/140798249,其中项目名为compute,并在该项目下创建一个名为app的应用,并且进行基本的配置。 2、导入Bootstrap前端框架 Bootstrap的使用参照https://blo…...

高性能并行计算面试-核心概念-问题理解
目录 1.什么是并行计算?高性能从哪些方面体现? 2.CPU常见的并行技术 3.GPU并行 4.并发与并行 5.常见的并行计算模型 6.如何评估并行程序的性能? 7.描述Am达尔定律和Gustafson定律,并解释它们对并行计算性能的影响 8.并行计…...

java-activiti笔记
版本:activiti7 <dependency><groupId>org.activiti</groupId><artifactId>activiti-json-converter</artifactId><version>7.0.0.Beta2</version><exclusions><exclusion><groupId>org.mybatis</g…...

Layui——隐藏表单项后不再进行验证
目录 修改后的部分代码 修改后的完整代码 我编辑用户信息和添加新用户用的是同一个表单,不同的是编辑用户信息里没有密码项和确认密码项,但是把它们隐藏后仍然要进行验证,也就是说它们俩的验证并没有随着表单项的隐藏而关闭。原因…...

Github Copilot 使用技巧
🎯目标读者 本文不包含如何安装 Github Copilot本文介绍了 Github Copilot 使用方法和一些技巧 本人已经使用 Github Copilot 2 年了,交了 3 次年费,每年 100$ 着实心痛,但是用着确实爽歪歪 但是感觉一直只用了一小部分功能&am…...