STM32F4_SPI协议详解

news2025/1/13 6:18:35

目录

1. 什么是SPI

2. SPI物理层

3. SPI协议层

3.1 SPI基本通讯过程

3.2 数据有效性

3.3 CPOL/CPHA及通讯模式

4. SPI框图及通讯过程

4.1 SPI框图

4.2 通讯过程

5. SPI初始化结构体

6. Flash芯片(W25Q128)简介

7. 库函数配置SPI1的主模式

8. 实验程序

8.1 实验程序讲解

8.1.1 main.c

8.1.2 SPI.c

8.1.3 SPI.h

8.1.4 W25Q128.c

8.1.5 W25Q128.h


1. 什么是SPI

        SPI 是由摩托罗拉公司提出的通讯协议,即串行外设接口(Serial Peripheral Interface),SPI是一种高速、全双工(可以同时接收和发送)、同步通信的通信总线,被广泛的应用于ADC、MCU的通信过程中,其相对于IIC最显著的特点就是通讯的速度快,要求通讯速率较高的场合。所以对于通讯速率要求不高的场合,通常使用IIC;通讯速率要求较高的场合,通常使用SPI。 

同样的,和IIC协议一样,学习SPI协议,首先也是从其物理层和协议层开始。

2. SPI物理层

这里通过和IIC协议进行比较,来进一步学习SPI协议

IIC协议是通过寻址的方式来找到通讯的从机,那么SPI是通过什么方式呢?

SPI不同于IIC,不是通过寻址的方式来和从机进行通信的,每个从机都挂载着一条独立的SS信号线,该信号线也称为从设备选择信号线,常常称作片选信号线,也称为NSS,CS(其中STM32F4的芯片引脚就是CS)。每个从机的SS信号线独占主机的一个引脚,也可以说是,有多少个从设备,主机上就有多少条片选信号线。将该片选信号线置低电平0,则意味着选择该从机进行SPI通信;将该片选信号线置高电平1,则意味着停止通讯

        事实上,每个从设备都有独立的这一条SS信号线,该信号线独占主机的一个引脚。IIC协议中通过设备地址来寻址,选中总线上某个设备并与其进行通讯;而SPI协议中没有设备地址,它是用SS信号线来寻址,当主机选中从设备时,就将所选的从设备的信号线置为低电平0,即意味着该设备被选中,即片选有效,接着主机开始与被选中的从设备进行SPI通讯。所以SPI通讯以SS线置低电平为开始信号,以SS线被拉高作为结束信号

IIC通讯只需要2根串行总线即可,SPI通讯的主机需要哪些特殊的引脚呢?

SCK(Serial Clock):时钟信号线,用于通讯数据同步。它由通讯的主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不同,STM32的SPI时钟频率最大为f_{pclk}/2,两个设备之间通讯时,通讯速率受限于低速设备。

MOSI(Master Output,Slave Input):主设备输出/从设备输入引脚。主机的设备从这条信号线输出,从机由这条信号线读入主机发送的数据,即这条信号线上数据的方向为主机到从机。

MISO(Master Input,Slave Output):主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。

注意:也正是因为这两条MOSI和MISO信号的存在,SPI通讯才能实现全双工,也就是发送和接收可以同步进行,大大增强了通讯的效率。因为IIC通讯只有一条串行数据总线SDA,一根线是不可能单方向的发送,同时也能反方向的接收数据的。而SPI通讯的两根数据线,一根可以用来发送数据,另外一根可以用来接受数据。

3. SPI协议层

SPI协议定义了通讯的起始和停止信号数据有效性时钟同步等环节。

3.1 SPI基本通讯过程

NSS信号,也就是上面提及的SS信号、CS信号,这三个信号表示的意义是相同的,都表示从机片选信号。NSS信号线由高到低,是SPI通讯的起始信号。NSS是每个从机各自独占的信号线,当从机在自己的NSS线检测到起始信号后,就知道自己被主机选中了,开始准备与主机进行通讯。

:NSS信号由低变高,是SPI通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。

②③④⑤:分别表示触发、采样、触发、采样。SCL高电平期间,不管是MOSI还是MISO输入或者输出,此时信号正在传动,处于不稳定状态,不进行采样。当SCL低电平期间,主从传输的数据进入稳定状态,这个时间段进行采样

3.2 数据有效性

SPI使用MOSI及MISO信号线来传输数据,使用SCK信号线进行数据同步。

MOSI及MISO数据线在SCK的每个时钟周期传输一位数据,如果需要传输8次,则需要循环传输,且数据输入输出是同时进行的。

3.3 CPOL/CPHA及通讯模式

由CPOL及CPHA的不同状态,SPI分成了四种模式,主机与从机需要工作在相同模式下才可以正常通讯,实际中采用较多的是模式0和模式3

4. SPI框图及通讯过程

4.1 SPI框图

 

通讯引脚:STM32F4板载了SPI1,SPI2,SPI3。通常情况下使用SPI1,传输速度较快。SPI2和SPI3可以作为I2S音频协议使用。

时钟控制逻辑:通过控制SPI的BR寄存器来控制SPI协议的波特率。产生SPI时钟。BR[0:2]位控制f_{pclk}时钟的分频因子,对f_{pclk}的分频结果就是SCK引脚的输出时钟频率。

数据控制逻辑:SPI的MOSI及MISO都连接到数据移位寄存器上,数据移位寄存器的数据来源于接收缓冲区及发送缓冲区。

通过写SPI的数据寄存器DR把数据填充到发送缓冲区中。

通过读数据寄存器DR,可以获取接收缓冲区中的内容。

数据帧长度可以通过控制寄存器CR1DFF位配置成8位及16位。

模式配置LSBFIRST可选择MSB先行还是LSB先行

4.2 通讯过程

5. SPI初始化结构体

跟其他外设一样,STM32标准库提供了SPI初始化结构体及初始化函数来配置SPI外设。

typedef struct
{
    uint16_t SPI_Direction; //设置SPI单双向模式
    uint16_t SPI_Mode;  //设置SPI主/从机模式
    uint16_t SPI_DataSize;  //设置SPI的数据帧长度,可选8/16位
    uint16_t SPI_CPOL;   //设置时钟极性,可选高低电平
    uint16_t SPI_CPHA;   //设置时钟相位,可选奇偶边沿采样
    uint16_t SPI_NSS;  //设置NSS引脚由SPI硬件控制还是软件控制
    uint16_t SPI_BaudRatePrescaler;  //设置时钟分频因子,fpclk/分频数=fSCK
    uint16_t SPI_FirstBit;   //设置MSB/LSB先行  设置高位先行还是低位先行
    uint16_t SPI_CRCPolynomial;  //设置CRC校验的表达式
}SPI_InitTypeDef;

SPI_Direction:设置SPI通讯方向,可设置为双线全双工SPI_Direction_2Lines_FullDuplex,双线只接收SPI_Direction_2Lines_RxOnly,单线只接收SPI_Direction_1Line_Rx,单线只发送SPI_Direction_1Line_Tx。

SPI_Mode:设置SPI工作在主机模式SPI_Mode_Master,还是从机模式SPI_Mode_Slave。注意:这两个模式的最大区别就是如果设置为主机模式,那么时钟SCK的时序是由通讯的主机产生的,如果设置为从机模式,那么SCK时钟的时序是由外来的SCK信号提供的。

SPI_DataSize:设置SPI通讯时数据帧的大小是8位SPI_DataSize_8b还是16位SPI_DataSize_16b。

SPI_CPOL:设置时钟极性,高电平SPI_CPOL_High,低电平SPI_CPOL_Low。

SPI_CPHA:设置时钟相位,SCK奇数边沿采集SPI_CPHA_1Edge,SCK偶数边沿采集SPI_CPHA_2Edge。

SPI_NSS:配置NSS引脚的使用模式,可以选择为硬件模式SPI_NSS_Hard,软件模式SPI_NSS_Soft。硬件模式下SPI片选信号由SPI硬件自动产生,软件模式则需要亲自把相应的GPIO端口拉高或置低产生非片选和片选信号。

SPI_BaudRatePrescaler:设置波特率分频因子,分频后时钟即为SPI的SCK信号线的时钟频率。

SPI_FirstBit:配置串行的通讯协议是高位在前还是低位在前。

SPI_CRCPolynomial:SPI计算CRC校验的多项式,若我们使用CRC校验,就使用这个成员的参数,来计算CRC的值。

6. Flash芯片(W25Q128)简介

        W25Q128 是华邦公司推出的大容量SPI FLASH产品,W25Q128的容量为128Mb,该系列还有W25Q80/16/32/64等。ALIENTEK所选择的W25Q128容量为128Mb,也就是16M字节。

        W25Q128 将16M的容量分为256个块(Block),每个块的大小为64K字节,每个块又分为16个扇区Sector,每个扇区4K个字节。W25Q128的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。因此,我们必须给W25Q128开辟一个至少4K的缓存区,这样对SRAM要求比较高,要求芯片必须有4K以上SRAM才能很好的操作。

        W25Q128 的擦写周期多达10W次,具有20年的数据保存期限,支持电压为2.7~3.6V,W25Q128支持标准的SPI,最大的SPI时钟可以到80MHz。

Flash的存储特性

1. 在写入数据前必须先擦除。

2. 擦除时会把数据位全重置为1。

3. 写入数据时只能把为1的数据改为0。

4. 擦除时必须按照最小单元来擦除。(一般是整个扇区)

7. 库函数配置SPI1的主模式

STM32F4的SPI功能很强大,SPI 时钟最高可以到 37.5MHz ,支持 DMA,可以配置为 SPI 协议或者 I2S 协议

1. 配置相关引脚的复用功能,使能SPI1时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);       //使能 SPI1 时钟

SPI1_MISO、SPI1_MOSI以及SPI1_SCK连接在PB3、PB4、PB5上,所以采用库函数将对应的引脚全部复用为SPI1

GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1);      //PB3 复用为 SPI1

GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1);      //PB4 复用为 SPI1

GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1);      //PB5 复用为 SPI1

初始化GPIO的结构体中,将对应的Mode设置为复用

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;      //复用功能

2. 初始化SPI1结构体,设置SPI1工作模式等

void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);      //初始化SPI函数

//该结构的各个参数详情参照该文上述的讲解

typedef struct 
{ 
 uint16_t SPI_Direction; 
 uint16_t SPI_Mode; 
 uint16_t SPI_DataSize; 
 uint16_t SPI_CPOL; 
 uint16_t SPI_CPHA; 
 uint16_t SPI_NSS; 
 uint16_t SPI_BaudRatePrescaler; 
 uint16_t SPI_FirstBit; 
 uint16_t SPI_CRCPolynomial; 
}SPI_InitTypeDef;

//SPI结构体的各个成员的配置如下

SPI_InitTypeDef SPI_InitStructure; 
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双线双向全双工 
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //主 SPI 
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // SPI 发送接收 8 位帧结构 
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;//串行同步时钟的空闲状态为高电平 
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;//第二个跳变沿数据被采样 
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS 信号由软件控制 
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //预分频 256 
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //数据传输从 MSB 位开始 
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC 值计算的多项式 
SPI_Init(SPI2, &SPI_InitStructure); //根据指定的参数初始化外设 SPIx 寄存器

3. 使能SPI1

SPI_Cmd(SPI1, ENABLE);      //使能 SPI1 外设

4. SPI传输数据

SPI通讯接口需要有发送数据接收数据的函数。

void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);     //往SPIx数据寄存器写入数据Data,实现发送。

uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx) ;      //从SPIx数据寄存器读出接收到的数据。

5. 查看SPI传输状态

SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE);     //获取状态位,判断数据是否传输完成,发送区是否为空

8. 实验程序

实验现象开机的时候先检测W25Q128是否存在,main函数检测按键,KEY1执行写入W25Q128操作,KEY0执行读出W25Q128操作,在LCD上显示相关信息。

注:W25Q128中头文件的指令表来自于下述表格

8.1 实验程序讲解

本实验程序基于STM32F4开发板。

实验现象:开机的时候先检测W25Q128是否存在,然后在主循环里面检测两个按键,其中按键KEY1用来执行写入W25Q128的操作,另外一个按键KEY0用来执行读出操作,在LCD上显示相关信息。

注意:

//注意

u16 W25QXX_ReadID(void)
{
	u16 Temp=0; //定义一个16位的返回值
	W25Q128_CS=0;  //使能片选
	SPI1_ReadWriteByte(0x90);  //发送读取ID命令 0x90
	SPI1_ReadWriteByte(0x00);
	SPI1_ReadWriteByte(0x00);
	SPI1_ReadWriteByte(0x00);
	//连续三次SPI1_ReadWriteByte(0x00);是读取器件ID的要求,伪字节定义为0x00
	Temp|=SPI1_ReadWriteByte(0xFF)<<8;  //因为定义的是16位的读ID函数,SPI1_ReadWriteByte函数类型是8位,所以读0xff放到高8位上
	Temp|=SPI1_ReadWriteByte(0xFF);
	W25Q128_CS=1;
	return Temp;
	
}

该函数读三次0x00 是读器件ID的要求

8.1.1 main.c

#include "stm32f4xx.h"                 
#include "delay.h"
#include "usart.h"
#include "LED.h"
#include "lcd.h"
#include "Key.h"
#include "usmart.h"
#include "W25Q128.h"
#include "SPI.h"

//LCD状态设置函数
void led_set(u8 sta)//只要工程目录下有usmart调试函数,主函数就必须调用这两个函数
{
	LED1=sta;
}
//函数参数调用测试函数
void test_fun(void(*ledset)(u8),u8 sta)
{
	led_set(sta);
}
//要写到W25Q128的字符串数组
const u8 TEXT_Buffer[]={"Explorer STM32F4 SPI TEST"};
#define SIZE sizeof(TEXT_Buffer)

int main(void)
{
	u8 key;
	u16 i=0;
	u8 datatemp[SIZE];  //因为要将 Explorer STM32F4 SPI TEST 写入到W25Q128中,所以要定义一个数组来接收,数组的大小为这串字符的大小
	u32 FLASH_SIZE;
	u16 ID=0;
	
	delay_init(168);
	uart_init(115200);  //切记:初始化延迟和串口要放在初始化LCD之前,否则初始化的LCD是没有任何显示结果的
	LED_Init();
	LCD_Init();
	Key_Init();
	W25Q128_Init();
	
	POINT_COLOR=RED;
	LCD_ShowString(30,50,200,16,16,"Explorer STM32F4");
	LCD_ShowString(30,70,200,16,16,"SPI TEST");
	LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
	LCD_ShowString(30,110,200,16,16,"2023/20/23");
	LCD_ShowString(30,130,200,16,16,"KEY1:Write  KEY0:Read");  //显示提示信息
	
	while(1)
	{
		ID=W25QXX_ReadID();
		if(ID==W25Q128||ID==NM25Q128)			//  0XEF17   0X5217
		{
			break;
		}
		LCD_ShowString(30,150,200,16,16,"W25Q128 Check Failed!");
		delay_ms(500);
		LCD_ShowString(30,150,200,16,16,"Please Check!        ");
		delay_ms(500);
		LED0=!LED0;    //LED0闪烁
	}
	LCD_ShowString(30,150,200,16,16,"W25Q128 Ready!");
	FLASH_SIZE=16*1024*1024;   //FLASH大小为16个字节
	POINT_COLOR=BLUE;   
	while(1)
	{
		key=KEY_Scan(0);
		if(key==2)  //表示KEY1按下
		{
			LCD_Fill(0,170,239,319,WHITE);   //清除半屏
			LCD_ShowString(30,170,200,16,16,"Start Write W25Q128……");
			W25QXX_Write((u8*)TEXT_Buffer,FLASH_SIZE-100,SIZE);//从倒数第100个地址处开始写入SIZE字节长的数据
			//W25QXX_Write第一个参数:数据存储区,全局变量设置的数组TEXT_Buffer用来存储
			//W25QXX_Write第二个参数:开始写入的地址,本次从倒数第100个地址处开始写入SIZE字节长的数据
			//FLASH_SIZE:该变量用来存储FLASH的字节大小
			//W25QXX_Write第三个参数:要写入的字节大小  #define SIZE sizeof(TEXT_Buffer)
			LCD_ShowString(30,170,200,16,16,"W25Q128 Write Finished!");
		}
		if(key==1)  //表示KEY0按下
		{
			LCD_ShowString(30,170,200,16,16,"Start Read W25Q128…… ");
			W25Q128_Read(datatemp,FLASH_SIZE-100,SIZE);//从倒数第100个地址处开始读,读出SIZE个字节
			//第一个参数:pBuffer:数据存储区  , 把读到的数据存储在事先定义好的变量datatemp中,然后再用LCD直接显示出datatemp
			//第二个参数:ReadAddress:开始读取的地址
			//第三个参数:Num:要读取的字节数,也就是要读取的长度
			LCD_ShowString(30,170,200,16,16,"The Data Readed Is:    ");	  //提示传送完成
			LCD_ShowString(30,190,200,16,16,datatemp);      //显示读到的字符串
		}
		i++;
		delay_ms(10);
		if(i==20)
		{
			LED0=!LED0;   //提示程序正在运行
			i=0;
		}
	}
}


	

8.1.2 SPI.c

#include "stm32f4xx.h"                 
#include "SPI.h"

//初始化SPI1
void SPI1_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE); //使能SPI1时钟
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);  //使能GPIOB时钟
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF;  //模式需要设置为复用
	GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;  //设置为推挽输出
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5;
	GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1);  //PB3复用为SPI1
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1);  //PB4复用为SPI1
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1);  //PB5复用为SPI1
	
	RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,ENABLE);  //复位SPI1
	RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,DISABLE);  //停止复位SPI1
	
	SPI_InitTypeDef SPI_InitStructure;
	SPI_InitStructure.SPI_Direction=SPI_Direction_2Lines_FullDuplex; //SPI双向全双工
	SPI_InitStructure.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_256;  //波特率预分频值256
	SPI_InitStructure.SPI_CPHA=SPI_CPHA_2Edge;  //串行时钟进行偶次采样
	SPI_InitStructure.SPI_CPOL=SPI_CPOL_High;  //串行时钟空闲状态为高电平
	SPI_InitStructure.SPI_CRCPolynomial=7;  //CRC值计算多项式
	SPI_InitStructure.SPI_DataSize=SPI_DataSize_8b;  //SPI数据帧8位
	SPI_InitStructure.SPI_FirstBit=SPI_FirstBit_MSB;  //数据传输高位在前
	SPI_InitStructure.SPI_Mode=SPI_Mode_Master;  //SPI主模式,即时钟时序是由主机SCK提供的
	SPI_InitStructure.SPI_NSS=SPI_NSS_Soft;  //NSS信号由软件管理
	SPI_Init(SPI1,&SPI_InitStructure);
	
	SPI_Cmd(SPI1,ENABLE);  //使能SPI时钟
	
	SPI1_ReadWriteByte(0xff);//启动传输
}
//SPI1速度设置函数
//SPI速度=fAPB2/分频系数
//SPI_BaudRate_Prescaler: 范围  SPI_BaudRatePrescaler_2~SPI_BaudRatePrescaler_256 
//fAPB2时钟一般为84MHz

//SPI_BaudRatePrescaler_4
//SPI_BaudRatePrescaler_8
//SPI_BaudRatePrescaler_16
//SPI_BaudRatePrescaler_32
//SPI_BaudRatePrescaler_64
//SPI_BaudRatePrescaler_128
//SPI_BaudRatePrescaler_256
void SPI1_SetSpeed(u8 SPI_BaudRate_Prescaler)
{
	//#define assert_param(expr) ((void)0)
	
	assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler)); //判断有效性 assert_param是STM32库函数判断有效性的函数,如果波特率预分频值不在上述的范围内,就会给程序员报错
	//位带操作主要分两步,第一步将所要操作的寄存器的某一位清零  第二步将寄存器清零的相关位写入所需的值
	SPI1->CR1&=0xFFC7; //0xFFC7对应于 1111 1111 1100 0111,相与也就表示将CR1寄存器的3-5位清零
	SPI1->CR1|=SPI_BaudRate_Prescaler;  //设置SPI速度,上一步清零,这一步将所需位写入相关的值
	
	SPI_Cmd(SPI1,ENABLE);  //使能SPI1
}
//SPI1读写一个字节
//WriteData:要写入的字节
//返回值:读到的字节
u8 SPI1_ReadWriteByte(u8 WriteData)
{
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)==RESET);  //通过之前的学习,TXE为状态位用来判断缓存寄存器是否为空
	//TXE若为0,则表示缓存寄存器非空;TXE若为1,则表示缓存寄存器空,只要跳出while循环,意味着TXE=1,缓存寄存器为空,可以写入下一个数值了
	SPI_I2S_SendData(SPI1,WriteData);  //通过SPI1发送一个字节的数据
	
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)==RESET);
	//RXNE是用来判断接收缓存区是否为空的状态位,其状态位标志和TXE相同
	return SPI_I2S_ReceiveData(SPI1); //返回通过SPI接收的数据
}



8.1.3 SPI.h

#ifndef _SPI__H_
#define _SPI__H_

void SPI1_Init(void);
void SPI1_SetSpeed(u8 SPI_BaudRate_Prescaler);
u8 SPI1_ReadWriteByte(u8 WriteData);

#endif

8.1.4 W25Q128.c

#include "stm32f4xx.h"
#include "W25Q128.h"
#include "delay.h"
#include "usart.h"
#include "SPI.h"

//W25Q80 
//W25Q16 
//W25Q32 
//W25Q64 
//W25Q128
u16 W25Q128_TYPE=W25Q128;   //默认是W25Q128

//4Kbytes为一个Sector
//16个扇区为1个Block
//W25Q128
//容量为16M字节,共有128个Block,4096个Sector

//初始化SPI FLASH的IO口
void W25Q128_Init(void)
{
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);  //使能GPIOB时钟
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG,ENABLE);  //使能GPIOG时钟
	
	//初始化GPIOB14,也就是F_CS片选信号线
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT;  //输出
	GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_14;
	GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	//GPIOG7 NRF_CS W25Q128 和 NRF24L01 共用 SPI1,所以这两个器件在使用的时候,必须分时复用(通过片选控制)才行。
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_7;
	GPIO_Init(GPIOG,&GPIO_InitStructure);
	
	GPIO_SetBits(GPIOG,GPIO_Pin_7);  //PG7是NRF24L01外设的片选信号线
	//意思就是STM32F4的SPI1接外设W25Q128 和 NRF24L01,目前SPI通信是一主多从,我们只需要W25Q128
	//所以需要将外设NRF24L01的片选信号线置1,表示关闭该外设,防止干扰SPI FLASH通信
	W25Q128_CS=1;//初始化默认关闭所有外设,也就是所有外设的片选信号线都置高电平
	
	SPI1_Init();
	SPI1_SetSpeed(SPI_BaudRatePrescaler_4); //总线时钟APB/2=fAPB1时钟 168/2=84M  84/4=21M时钟
	
	W25Q128_TYPE=W25QXX_ReadID(); //读取FLASH ID
}
//读W25Q128状态寄存器
//BIT7  6   5   4   3   2   1   0
//SPR   RV  TB BP2 BP1 BP0 WEL BUSY
//SPR:默认0,状态寄存器保护位,配合WP使用
//TB,BP2,BP1,BP0:FLASH区域写保护设置
//WEL:写使能锁定
//BUSY:忙标记位(1,忙;0,空闲)
//默认:0x00
u8 W25Q128_ReadSR(void)
{
	u8 byte=0;
	W25Q128_CS=0;  //使能片选信号
	SPI1_ReadWriteByte(W25X_ReadStatusReg);  //发送读取状态寄存器命令
	byte=SPI1_ReadWriteByte(0xff);   //读取一个字节
	W25Q128_CS=1;  //取消片选信号
	return byte;
}
//写W25Q128状态寄存器
//BIT7  6   5   4   3   2   1   0
//SPR   RV  TB BP2 BP1 BP0 WEL BUSY
//只有SPR,TB,BP2,BP1,BP0(bit 7,5,4,3,2)可以写
void W25Q128_Write_SR(u8 Byte)
{
	W25Q128_CS=0;
	SPI1_ReadWriteByte(W25X_WriteStatusReg);  //发送写取状态寄存器命令
	SPI1_ReadWriteByte(Byte);  
	W25Q128_CS=1;
	//写和读取的思想是一样的,读就是设置一个中间变量,通过调用读写函数,把读到值赋给这个变量,最后返回,注意,读一定首先给状态寄存器一个读取的命令
	//写也是首先给状态寄存器一个写的命令,然后通过调用读写函数将要写的值写入寄存器即可
}
//W25Q128写使能
//将WEL置位
void W25Q128_Write_Enable(void)
{
	W25Q128_CS=0;
	SPI1_ReadWriteByte(W25X_WriteEnable);  //发送写使能指令
	W25Q128_CS=1;
}
//W25Q128写禁止
//将WEL清零
void W25Q128_Write_Disable(void)
{
	W25Q128_CS=0;
	SPI1_ReadWriteByte(W25X_WriteDisable);  //发送写禁止指令
	W25Q128_CS=1;
}
//读取芯片ID
//返回值如下:				   
//0XEF13,表示芯片型号为W25Q80  
//0XEF14,表示芯片型号为W25Q16    
//0XEF15,表示芯片型号为W25Q32  
//0XEF16,表示芯片型号为W25Q64 
//0XEF17,表示芯片型号为W25Q128 
u16 W25QXX_ReadID(void)
{
	u16 Temp=0; //定义一个16位的返回值
	W25Q128_CS=0;  //使能片选
	SPI1_ReadWriteByte(0x90);  //发送读取ID命令 0x90
	SPI1_ReadWriteByte(0x00);
	SPI1_ReadWriteByte(0x00);
	SPI1_ReadWriteByte(0x00);
	//连续三次SPI1_ReadWriteByte(0x00);是读取器件ID的要求,伪字节定义为0x00
	Temp|=SPI1_ReadWriteByte(0xFF)<<8;  //因为定义的是16位的读ID函数,SPI1_ReadWriteByte函数类型是8位,所以读0xff放到高8位上
	Temp|=SPI1_ReadWriteByte(0xFF);
	W25Q128_CS=1;
	return Temp;
	
}
//读取SPI FLASH
//在指定地址上开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddress:开始读取的地址
//Num:要读取的字节数,也就是要读取的长度
void W25Q128_Read(u8 *pBuffer,u32 ReadAddress,u16 Num)
{
	u16 i;
	W25Q128_CS=0;
	SPI1_ReadWriteByte(W25X_ReadData);   //发送读取命令
	SPI1_ReadWriteByte((u8)((ReadAddress)>>16)); //发送24bit地址,本次发送16-23位
	SPI1_ReadWriteByte((u8)((ReadAddress)>>8));//本次发送8-15位
	SPI1_ReadWriteByte((u8)ReadAddress);  //本次发送0-7位
	for(i=0;i<Num;i++)
	{
		pBuffer[i]=SPI1_ReadWriteByte(0xFF);  //循环进行读数
		//每次接收一个字节,SPI通信,接收数据和发送数据是同时进行的,发送数据一般为0xFF,其实这个数据是可以任意的
		//0xFF=1111 1111,正好表示W25Q128的扇形区被擦除
	}
	W25Q128_CS=1;
}
//SPI在一页(0~65535)内写入少于256个字节的数据
//在指定地址开始写入最大256个字节的数据
//pBuffer:数据存储区
//WriteAddress:开始写入的地址
//Num:要写入的字节数,该字节数最大256,该数不应该超过该页的剩余字节数
void W25Q128_Write_Page(u8* pBuffer,u32 WriteAddress,u16 Num)
{
	u16 i;
	W25Q128_Write_Enable();  //写使能
	W25Q128_CS=0;
	SPI1_ReadWriteByte(W25X_PageProgram);  //发送写页命令
	SPI1_ReadWriteByte((u8)((WriteAddress)>>16));  //发送24位地址,同上一个函数,分三次发送,每次发送8位
	SPI1_ReadWriteByte((u8)((WriteAddress)>>8));
	SPI1_ReadWriteByte((u8)WriteAddress);
	for(i=0;i<Num;i++)
	{
		SPI1_ReadWriteByte(pBuffer[i]);   //循环写数
	}
	W25Q128_CS=1;  //取消片选
	W25Q128_Wait_Busy();    //等待写入结束
}
//无检验写SPI FLASH 
//必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!

//具有自动换页功能 !!!

//在指定地址开始写入指定长度的数据,但是要确保地址不越界!
//pBuffer:数据存储区
//WriteAddress:开始写入的地址(24bit)
//Num:要写入的字节数(最大65535)
//CHECK OK
void W25Q128_Write_NoCheck(u8* pBuffer,u32 WriteAddress,u16 Num)
{
	u16 pageremain;   //定义变量用来设置该页剩余的字节数
	pageremain=256-WriteAddress%256;   //单页剩余的字节数  WriteAddress%256表示在扇形区的偏移量
	if(Num<=pageremain)  //如果要写入的字节数小于单页剩余的字节数,就意味着剩余空间可以写下
		pageremain=Num;//直接将这一页剩余的字节数设置为要写入的字节数,确保地址不越界
	while(1)
	{
		W25Q128_Write_Page(pBuffer,WriteAddress,pageremain);//调用在一页上写字节函数,所写入的字节数为单页剩余的字节数(也就是我们想要写入的字节数  pageremain=Num;)
		if(Num==pageremain) //意味着要写入的字节数恰好等于这一页剩余的字节数,break这一页写结束了
			break;
		else  // Num>pageremain,否则就意味着要写入的字节数大于这一页剩余的字节数了
		{
			//首先明确要写入的字节数大于这一页剩余的字节数了,所以接下来要做的就是把剩余空间写满,不够的擦除,也可以说是另起一页接着写
			pBuffer=pBuffer+pageremain;   //初始化定义的数据存储区指针是指向扇形区的起始位置的,现在要从这一页的中间部分开始写 通过这句程序可以使指针pBuffer指向这一页的偏移地址(简单一点说就是:我们小时候写作业,上一次作业写到了一页的第5行第5个位置,那么下一次写作业本能的就从第5行第5个位置开始写,计算机是需要指针去寻找这个位置的,上述程序就是将指针定位到这个位置)
			//所以数据存储区指针应该保证接下来存储数据在剩余字节数的基础上写字节
			WriteAddress=WriteAddress+pageremain;//同理开始写入的地址也要保证随着单页剩余的字节数进行变化
			
			Num=Num-pageremain; //该程序意思就是减去已经写入的字节数,此时的Num表示下一页应该写入的字节数
			//else虽然这一页的剩余字节已经不够写了,但是还是要先计算出剩余字节写了多少,不够的字节写到下一页
			if(Num>256)  //因为一页最多只能写256个字节,如果下一页要写入的字节数大于256,只能先写满这一页,通过下一循环写剩余的
				pageremain=256;
			else  //如果要写入的字节数小于256,就意味着这一页是可以写完要写入的字节的
				pageremain=Num; //把要写入的字节给到该页剩余的字节数,随着W25Q128_Write_Page(pBuffer,WriteAddress,pageremain);函数进行写一页的操作
		}
	}
}
//写SPI FLASH
//在指定地址开始写入指定长度的数据

//该函数带擦除操作!!!

//pBuffer:数据存储区
//WriteAddress:开始写入的地址(24bit)						
//Num:要写入的字节数(最大65535)  
u8 W25Q128_BUFFER[4096];  //初始化全局变量W25Q128_BUFFER表示扇区能写入的最大字节数为4096
void W25QXX_Write(u8* pBuffer,u32 WriteAddress,u16 Num)
{
	u32 SecAddress;  //存储扇区地址
	u16 SecMove;  //存储扇区偏移量
	u16 SecRemain;  //存储扇区内剩余空间的大小
	u16 i;
	u8* W25Q128_BUF;
	W25Q128_BUF=W25Q128_BUFFER;  //定义8位的指针数组,用来存储所要写入的字节
	SecAddress=WriteAddress/4096;  //扇区地址(说白了就是一个扇区可以存放4096个字节,除以4096就表示到底放在了那个扇区,就是总字数除以每一页可以写入的字数就得到到底写在了哪一页)
	SecMove=WriteAddress%4096;  //在扇区内的偏移(也就是一页中存放在什么位置,这个位置是相对于起始位置而言的)
	SecRemain=4096-SecMove;    //扇区剩余空间大小(每个扇区能写入的最大字节数为4096,4096减去该扇区的偏移量就是该扇区内剩余空间的大小)
	//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用  这也是为什么要引头文件usart.h的原因
	//%X打印十六进制数,\r\n:换行符
	if(Num<=SecRemain)//如果要写入的字节数小于这一页剩余空间的大小,意味着这一页可以写下
		SecRemain=Num;//把要写入的字节数赋值给扇区剩余空间,调用W25Q128_Write_Page函数写即可
	while(1) //进入擦除操作
	{
		//pBuffer:数据存储区
		//ReadAddress:开始读取的地址
		//Num:要读取的字节数,也就是要读取的长度
		//void W25Q128_Read(u8 *pBuffer,u32 ReadAddress,u16 Num)
		W25Q128_Read(W25Q128_BUF,SecAddress*4096,4096);//读出整个扇区的内容
		for(i=0;i<SecRemain;i++) //校验
		{
			if(W25Q128_BUF[SecMove+i]!=0xFF) //因为W25Q128芯片只能在1的基础上改为0,擦除就意味着把整个扇形区写满1 
            //W25Q128_BUF[SecMove+i]表示从该扇区的偏移地址开始,一次向后遍历要写入字节长度个地址
				break;  //不等于0xFF就意味着需要擦除  偏移量加上i表示随时指向我想要写的地址
		}
		if(i<SecRemain)   //需要擦除
        //这里解释一下为什么 i<SecRemain 就需要擦除,首先看上面的for循环:一 i>SecRemain,跳出for循环;二是最主要的,break结束if判断语句,意味着从偏移量开始往后遍历不全是1,需要擦除;break直接跳出for循环,此时i的值处于一个分界线的状态,SecMove+i之前的地址上都是1,往后不全是1;
        //此时i<SecRemain,意味着 该扇区偏移量+i 之间的这块空间不足以存放要写入的字节,所以需要擦除整个扇区,在擦除之间还要首先进行数据的复制,防止从起始位置到偏移量的这块空间上的内容丢失;
//(这就好比我们写作业,0-100吧,0-50是我们之前写的作业,本次我们要再写30个数,那么就需要写到51-80,但是发现写到65时,纸张出现缺损,此时可以通过橡皮擦擦除整页,首先需要把0-50转移到下一页,然后从起始位置开始写51-80是一个道理)
		{
			W25Q128_Erase_Sector(SecAddress);  //调用擦除函数擦除这个扇区
			for(i=0;i<SecRemain;i++)  //复制
			{
				W25Q128_BUF[i+SecMove]=pBuffer[i]; //i+SecMove表示随时指向该页中随着剩余字节数递减的地址
				//把原本数据存储区的字节写到随剩余字节数偏移的地址上
			}
			W25Q128_Write_NoCheck(W25Q128_BUF,SecAddress*4096,4096);  //写入整个扇区
		}
		else  //否则意味着不需要擦除,直接在该页的剩余空间上写即可
			W25Q128_Write_NoCheck(pBuffer,WriteAddress,SecRemain); 
		if(Num==SecRemain)  //如果要写入的字节数恰好等于该页的剩余空间,这是我们最想要看到的情况
			break;  //表示写入结束
		else  //Num>SecRemain 否则表示要写入的字节大于该页剩余的字节了
		{
			SecAddress++;  //该页已经不够了,转下一页
			SecMove=0;   //偏移量为0,表示从下一页的起始位置开始写
			
			pBuffer=pBuffer+SecRemain;  //数据存储区指针跟着一并偏移
			WriteAddress=WriteAddress+SecRemain;//开始读取的地址也跟着一起偏移
			
			Num=Num-SecRemain;  //该程序建立在这一页不够写的基础上,虽然这一页不够写,但是我还是要计算这一页写了多少,有多少字节需要写入到下一页
			if(Num>4096)   //因为定义整个扇区可以写入的最大字节数是4096,如果要写入的字节Num大于4096,意味着下一个字节还是不够写
				SecRemain=4096;   //因为一个扇区只能写4096个字节,所以下一个扇区不够写的基础上,只能下下次循环写剩余的字节了
			else//else表示下一个字节完全够写了
				SecRemain=Num;  //把要写入的字节Num给到扇区剩余空间,调用写函数写即可
		}
	}
}
//擦除整个芯片		  
//等待时间超长...
void W25Q128_Erase_Chip(void)
{
	W25Q128_Write_Enable();  //写使能
	W25Q128_Wait_Busy();   //等待空闲
	W25Q128_CS=0;
	SPI1_ReadWriteByte(W25X_ChipErase);  //发送片擦除命令
	W25Q128_CS=1;
	W25Q128_Wait_Busy();   //等待芯片擦除结束
}
//擦除一个扇区
//SecAddress:扇区地址 根据实际容量设置
//擦除一个山区的最少时间:150ms
void W25Q128_Erase_Sector(u32 SecAddress)
{
	//监视Flash擦除情况,测试使用
	printf("fe:%x\r\n",SecAddress);
	SecAddress=SecAddress*4096;  //擦除整个扇区
	W25Q128_Write_Enable();  //写使能
	W25Q128_Wait_Busy();   //等待空闲
	W25Q128_CS=0; 
	SPI1_ReadWriteByte(W25X_SectorErase);  //发送扇区擦除命令
	SPI1_ReadWriteByte((u8)((SecAddress)>>16)); //发送24位地址,分三次发送,每次发送8位
	SPI1_ReadWriteByte((u8)((SecAddress)>>8));
	SPI1_ReadWriteByte((u8)SecAddress);
	W25Q128_CS=1;
	W25Q128_Wait_Busy();  //等待擦除完成
}
//等待空闲
void W25Q128_Wait_Busy(void)
{
	while((W25Q128_ReadSR()&0x01)==0x01);   //等待W25Q128状态寄存器的BUSY位清空
	//与&表示拿出SR最低位,也就是BUSY位,只要该位是1,就始终位于while循环中,只要跳出循环,该位就清0
}
//进入掉电模式
void W25Q128_PowerDown(void)
{
	W25Q128_CS=0;  
	SPI1_ReadWriteByte(W25X_PowerDown);  //发送掉电命令
	W25Q128_CS=1;
	delay_us(3);  
}
//唤醒
void W25Q128_WAKEUP(void)
{
	W25Q128_CS=0;  
	SPI1_ReadWriteByte(W25X_ReleasePowerDown);  //发送唤醒命令
	W25Q128_CS=1;
	delay_us(3); 
}


8.1.5 W25Q128.h

#ifndef _W25Q128__H_
#define _W25Q128__H_

//W25X系列/Q系列芯片列表	   
//W25Q80  ID  0XEF13
//W25Q16  ID  0XEF14
//W25Q32  ID  0XEF15
//W25Q64  ID  0XEF16	
//W25Q128 ID  0XEF17	
#define W25Q80 	0XEF13 	
#define W25Q16 	0XEF14
#define W25Q32 	0XEF15
#define W25Q64 	0XEF16
#define W25Q128	0XEF17

#define NM25Q80 	0X5213
#define NM25Q16 	0X5214
#define NM25Q32 	0X5215
#define NM25Q64 	0X5216
#define NM25Q128	0X5217
#define NM25Q256 	0X5218

extern u16 W25Q128_TYPE;				//定义W25Q128芯片型号		   

#define	W25Q128_CS 		PBout(14)  		//W25Q128的片选信号 ,采用位段命名PBout(14)


//指令表
#define W25X_WriteEnable		0x06 
#define W25X_WriteDisable		0x04 
#define W25X_ReadStatusReg		0x05 
#define W25X_WriteStatusReg		0x01 
#define W25X_ReadData			0x03 
#define W25X_FastReadData		0x0B 
#define W25X_FastReadDual		0x3B 
#define W25X_PageProgram		0x02 
#define W25X_BlockErase			0xD8 
#define W25X_SectorErase		0x20 
#define W25X_ChipErase			0xC7 
#define W25X_PowerDown			0xB9 
#define W25X_ReleasePowerDown	0xAB 
#define W25X_DeviceID			0xAB 
#define W25X_ManufactDeviceID	0x90 
#define W25X_JedecDeviceID		0x9F 


void W25Q128_Init(void);
u8 W25Q128_ReadSR(void);
void W25Q128_Write_SR(u8 Byte);
void W25Q128_Write_Enable(void);
void W25Q128_Write_Disable(void);
u16 W25QXX_ReadID(void);
void W25Q128_Read(u8 *pBuffer,u32 ReadAddress,u16 Num);
void W25Q128_Write_Page(u8* pBuffer,u32 WriteAddress,u16 Num);
void W25Q128_Write_NoCheck(u8* pBuffer,u32 WriteAddress,u16 Num);
void W25QXX_Write(u8* pBuffer,u32 WriteAddress,u16 Num);
void W25Q128_Erase_Chip(void);
void W25Q128_Erase_Sector(u32 SecAddress);
void W25Q128_Wait_Busy(void);
void W25Q128_PowerDown(void);
void W25Q128_WAKEUP(void);

#endif


对该文内容有疑问, 或者内容上有写的不对的地方,欢迎批评更改!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/601787.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

“金九银十”是找工作的最佳时期吗?那倒未必

金九银十找工作 优势&#xff1a; 供选择的公司多&#xff0c;机会多 劣势&#xff1a; 人才供应量旺盛 成为备胎的几率大增&#xff0c;获取offer的时间较慢 若无明显竞争力&#xff0c;薪资涨幅相对不会太高 比起那些在跳槽季(金三银四&#xff0c;金九银十)扎堆找工作…

【LED子系统深度剖析】九、数据结构详解(番外篇)

个人主页:董哥聊技术 我是董哥,高级嵌入式软件开发工程师,从事嵌入式Linux驱动开发和系统开发,曾就职于世界500强公司! 创作理念:专注分享高质量嵌入式文章,让大家读有所得! 文章目录 1、核心数据结构1.1 gpio_led_platform_data1.2 gpio_leds_priv1.3 gpio_led1.4 gpi…

2022年营收31.88亿,国产模拟 IC 头部企业持续扩充品类促发展

国产IC增速快于全球 IC &#xff0c; 国产替代空间广阔 根据 WSTS 的数据&#xff0c;2021 年全球 IC 市场规模高增 28.2%&#xff0c;2022 年全球 IC 市场规模同比增速放缓至 3.7%&#xff0c;由于需求减弱&#xff0c;且全球各下游仍在消化库存&#xff0c;预计 2023 年全球…

浮点数在内存中的存储以及用指针改变内存与强制转换的区别

文章目录 浮点型在内存中的存储引例浮点数的表示形式浮点数的存储E不全为零且E不全为1E全为0E全为1 Eg 总结用指针改变内存和强制转换的区别 浮点型在内存中的存储 引例 我们先来看下面一段代码 #include<stdio.h>int main() {int n 9;float* pFloat (float*)&n;p…

【来不及刷题之】33、合并区间(+ 删除被覆盖区间)

1. 删除被覆盖区间 先来看与合并区间比较相似的一道题目&#xff1a;删除被覆盖的区间 思路&#xff1a; 起点按照升序排序&#xff0c;终点按照降序排序基于以上排序规则&#xff0c;相邻的两个区间会有以下三种情况&#xff1a; 第一种情况&#xff1a;找到一个覆盖区间&a…

axios封装时对config参数的一点思考

目 录 0、起因1、冷静分析2、一个简单粗糙但是能用的封装 0、起因 创建一个实例&#xff1a; const service axios.create({baseURL: "/api",timeout: 3e3,withCredentials: true,headers:{"Content-Type": "application/json"} })简单封装&a…

STM32控制OLED介绍

OLED&#xff0c;即有机发光二极管&#xff08;Organic Light-Emitting Diode&#xff09;&#xff0c;又称为有机电激光显示&#xff08;Organic Electroluminesence Display&#xff0c; OELD&#xff09;。 OLED 由于同时具备自发光&#xff0c;不需背光源、对比度高、 厚度…

人工智能(pytorch)搭建模型7-利用pytorch搭建一个BiLSTM+CRF模型,实现简单的命名实体识别

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下人工智能(pytorch)搭建模型7-利用pytorch搭建一个BiLSTMCRF模型&#xff0c;实现简单的命名实体识别&#xff0c;BiLSTMCRF 模型是一种常用的序列标注算法&#xff0c;可用于词性标注、分词、命名实体识别等任务。本…

软件安全概述

软件定义是&#xff1a;计算机程序、规则和可能相关的文档。 软件是程序、数据和文档的集合体。 零日漏洞、零日攻击 零日漏洞是指未被公开披露的软件漏洞&#xff0c;没有给软件的作者或厂商以时间去为漏洞打补丁或是给出建议解决方案&#xff0c;从而攻击者能够利用这种漏洞破…

ROS:话题消息(Message)的定义与使用

目录 一、话题模型二、自定义话题消息2.1定义msg文件2.2在package.xml中添加功能包依赖2.3在CMakeLists.txt中添加编译选项2.4编译生成C头文件或Python库 三、创建代码并编译运行&#xff08;C&#xff09;3.1创建代码3.2编译 四、运行 一、话题模型 自定义一个消息类型“Pers…

python接口测试之测试报告

在本文章中&#xff0c;主要使用jenkins和编写的自动化测试代码&#xff0c;来生成漂亮的测试报告&#xff0c;关于什么是CI这些我就不详细的介绍了&#xff0c;这里我们主要是实战为主。 首先搭建java的环境&#xff0c;这个这里不做介绍。搭建好java的环境后&#xff0c;在h…

Python:Python编程:从入门到实践__超清版:Python标准库:线程

Python线程与安全 实现线程安全有多重方式&#xff0c;常见的包括&#xff1a;锁&#xff0c;条件变量&#xff0c;原子操作&#xff0c;线程本地存储等。 &#x1f49a; 1. 锁2. 条件变量3. 通过 join 阻塞当前线程4. 采用 sleep 来休眠一段时间5. 原子操作5.1 使用 threading…

【I2C】Linux I2C子系统分析

文章目录 一、I2C体系架构二、主要的结构体1. i2c_adapter2. i2c_algorithm3. i2c_driver4. i2c_client4.1 方式一&#xff1a;通过I2C bus number静态方式来创建4.2 方式二&#xff1a;通过Device Tree来创建4.3 方式三&#xff1a;直接通过i2c_new_device来创建4.3 方式四&am…

openEuler22.03制作openstack平台使用的镜像

系列文章目录 第一章 openEuler22.03制作openstack平台使用的镜像 文章目录 系列文章目录前言一、virt-manager上的准备工作1、网卡类型切换为virtio2、IDE驱动设置成Virtio3、Display设置成vnc3、虚拟机系统分区 二、安装普通工具包三、安装云化工具包1、安装工具包2、修改配…

数字化转型,企业为什么要转型?如何转型?

数字化转型是利用数字化技术&#xff08;例如云计算、大数据、人工智能、物联网、区块链等&#xff09;和能力来驱动组织商业模式创新和商业生态系统重构的途径和方法即是数字化转型。其目的是实现企业业务的转型、创新、增长。 核心强调了两点&#xff0c;其一是数字化技术的应…

每日一练 | 华为认证真题练习Day51

1、如下图所示&#xff0c;IPSec传输模式中AH的头部应该插入到以下哪个位置&#xff1f; A. 1 B. 2 C. 3 D. 4 2、以下哪种远程登录方式最安全&#xff1f; A. Telnet B. Stelnet v100 C. Stelnet v2 D. Stelnet v1 3、以下业务模块的ACL默认动作为permit的是&#xff1…

玩转 ChatGPT,看这条就够了,Prompt 最全中文合集

Prompt 最全中文合集 玩转 ChatGPT&#xff0c;看这条就够了&#xff01; &#x1f680; 简化流程&#xff1a;ChatGPT Shortcut 提供了快捷指令表&#xff0c;可以快速筛选和搜索适用于不同场景的提示词&#xff0c;帮助用户简化使用流程。 &#x1f4bb; 提高生产力&#…

CSDN打出各种数学符号和数学公式

目录 1、基本四则运算2、指数对数3、根号、省略号、向量4、大&#xff08;小&#xff09;于等于号5、特殊符号、希腊字母符号6、累加累乘7、矩阵8、更改公式中的颜色 我们在用CSDN打出各种数学符号和数学公式时&#xff0c;需要学习一些关于LaTex的语法&#xff0c;在此做一个记…

java数组学习

一、数组的概述 1.数组的理解:数组(Array),是多个相同类型数据按一定顺序排列的集合&#xff0c; 并使用一个名字命名&#xff0c;并通过编号的方式对这些数据进行统一管理。 2.数组相关的概念: >数组名 >元素 >角标、下标、索引 >数组的长度&#xff1a;元素…

联通云数据库CUDB:基于openGauss打造新一代自主创新云原生数据库

总体概述 联通云彰显央企担当&#xff0c;围绕国家对信息技术基础软件的政策要求&#xff0c;开展数据库自主研发。在openGauss开源社区版软件基础上&#xff0c;聚焦政企市场&#xff0c;坚持内核创新&#xff0c;完善工具生态&#xff0c;基于海量云存储能力、存算分离架构…