精华内容
下载资源
问答
  • 基于C语言的Q格式使用详解
    千次阅读 多人点赞
    更多相关内容
  • w25q128fv中文手册 w25q256fv中文手册

    热门讨论 2015-07-01 20:11:35
    w25q128fv中文手册 w25q256fv中文手册 256可以参考128的 完全中文手册
  • SPI总线:STM32本身支持SPI硬件时序,本文示例代码里同时采用模拟时序和硬件时序两种方式读写W25Q64。 模拟时序更加方便移植到其他单片机,更加方便学习理解SPI时序,通用性更高,不分MCU; 硬件时序效率更高,每个...

    一、环境介绍

    编程软件: keil5

    操作系统: win10

    MCU型号: STM32F103ZET6

    STM32编程方式: 寄存器开发 (方便程序移植到其他单片机)

    SPI总线:  STM32本身支持SPI硬件时序,本文示例代码里同时采用模拟时序和硬件时序两种方式读写W25Q64。

    模拟时序更加方便移植到其他单片机,更加方便学习理解SPI时序,通用性更高,不分MCU;

    硬件时序效率更高,每个MCU配置方法不同,依赖MCU硬件本身支持。

    存储器件: 采用华邦W25Q64  flash存储芯片。 

    W25Q64这类似的Flash存储芯片在单片机里、嵌入式系统里还是比较常见,可以用来存储图片数据、字库数据、音频数据、保存设备运行日志文件等。

    完整工程代码下载:https://download.csdn.net/download/xiaolong1126626497/19425042

    二、华邦W25Q64介绍(FLASH存储类型)

    2.1 W25Q64芯片功能介绍

    W25Q64是为系统提供一个最小空间、最少引脚,最低功耗的串行Flash存储器,25Q系列比普通的串行Flash存储器更灵活,性能更优越。

    W25Q64支持双倍/四倍的SPI,可以储存包括声音、文本、图片和其他数据;芯片支持的工作电压 2.7V 到 3.6V,正常工作时电流小于5mA,掉电时低于1uA,所有芯片提供标准的封装。 

    W25Q64的内存空间结构:  一页256字节,4K(4096 字节)为一个扇区,16个扇区为1块,容量为8M字节,共有128个块,2048 个扇区。  

    W25Q64每页大小由256字节组成,每页的256字节用一次页编程指令即可完成。

    擦除指令分别支持: 16页(1个扇区)、128页、256页、全片擦除。 

    W25Q64支持标准串行外围接口(SPI),和高速的双倍/四倍输出,双倍/四倍用的引脚:串行时钟、片选端、串行数据 I/O0(DI)、I/O1(DO)、I/O2(WP)和 I/O3(HOLD)。
    SPI 最高支持 80MHz,当用快读双倍/四倍指令时,相当于双倍输出时最高速率160MHz,四倍输出时最高速率 320MHz。这个传输速率比得上8位和16位的并行Flash存储器。
     
    W25Q64支持 JEDEC 标准,具有唯一的 64 位识别序列号,方便区别芯片型号。

     2.2 W25Q64芯片特性详细介绍

    ●SPI串行存储器系列    
    -W25Q64:64M 位/8M 字节    
    -W25Q16:16M 位/2M 字节    
    -W25Q32:32M 位/4M 字节    
    -每 256 字节可编程页    

        
    ●灵活的4KB扇区结构     
    -统一的扇区擦除(4K 字节)     
    -块擦除(32K 和 64K 字节) 
    -一次编程 256 字节 
    -至少 100,000 写/擦除周期 
    -数据保存 20 年 
        
    ●标准、双倍和四倍SPI 
    -标准 SPI:CLK、CS、DI、DO、WP、HOLD         
    -双倍 SPI:CLK、CS、IO0、IO1、WP、HOLD         
    -四倍 SPI:CLK、CS、IO0、IO1、IO2、IO3 

    ●高级的安全特点 
    -软件和硬件写保护 
    -选择扇区和块保护 
    -一次性编程保护(1) 
    -每个设备具有唯一的64位ID(1) 

    ●高性能串行Flash存储器     
    -比普通串行Flash性能高6倍          
    -80MHz时钟频率          
    -双倍SPI相当于160MHz         
    -四倍SPI相当于320MHz         
    -40MB/S连续传输数据     
    -30MB/S随机存取(每32字节)     
    -比得上16位并行存储器
             
    ●低功耗、宽温度范围 
    -单电源 2.7V-3.6V 
    -工作电流 4mA,掉电<1μA(典型值)
    -40℃~+85℃工作 

    2.3  引脚介绍

    下面只介绍W25Q64标准SPI接口,因为目前开发板上的封装使用的就是标准SPI接口。

     

    引脚编号

    引脚名称

    I/O

    功能

    1

    /CS

    I

    片选端输入

    2

    DO(IO1)

    I/O

    数据输出(数据输入输出 1)*1 

    3

    /WP(IO2)

    I/O

    写保护输入(数据输入输出 2)*2 

    4

    GND

     

    5

    DI(IO0)

    I/O

    数据输入(数据输入输出 0)*1 

    6

    CLK

    I

    串行时钟输入

    7

    /HOLD(IO3)

    I/O

    保持端输入(数据输入输出 3)*2 

    8

    VCC

     

    电源

      2.2.1 SPI片选(/CS)引脚用于使能和禁止芯片操作

    CS引脚是W25Q64的片选引脚,用于选中芯片;当CS为高电平时,芯片未被选择,串行数据输出(DO、IO0、IO1、IO2 和 IO3)引脚为高阻态。未被选择时,芯片处于待机状态下的低功耗,除非芯片内部在擦除、编程。当/CS 变成低电平,芯片功耗将增长到正常工作,能够从芯片读写数据。上电后, 在接收新的指令前,/CS 必须由高变为低电平。上电后,/CS 必须上升到 VCC,在/CS 接上拉电阻可以完成这个操作。 

    2.2.2 串行数据输入、输出和 IOs(DI、DO 和 IO0、IO1、IO2、IO3)

    W25Q64、W25Q16 和 W25Q32 支持标准 SPI、双倍 SPI 和四倍 SPI。

    标准的 SPI 传输用单向的 DI(输入)引脚连续的写命令、地址或者数据在串行时钟(CLK)的上升沿时写入到芯片内。

    标准的SPI 用单向的 DO(输出)在 CLK 的下降沿从芯片内读出数据或状态。 

    2.2.3 写保护(/WP)

    写保护引脚(/WP)用来保护状态寄存器。和状态寄存器的块保护位(SEC、TB、BP2、BP1 和BP0)和状态寄存器保护位(SRP)对存储器进行一部分或者全部的硬件保护。/WP 引脚低电平有效。当状态寄存器 2 的 QE 位被置位了,/WP 引脚(硬件写保护)的功能不可用。

    2.2.4  保持端(/HOLD)

    当/HOLD 引脚是有效时,允许芯片暂停工作。在/CS 为低电平时,当/HOLD 变为低电平,DO 引脚将变为高阻态,在 DI 和 CLK 引脚上的信号将无效。当/HOLD 变为高电平,芯片恢复工作。/HOLD 功能用在当有多个设备共享同一 SPI 总线时。/HOLD 引脚低电平有效。当状态寄存器 2 的 QE 位被置位了,/ HOLD 引脚的功能不可用。

    2.2.5 串行时钟(CLK)

    串行时钟输入引脚为串行输入和输出操作提供时序。(见 SPI 操作)。

    设备数据传输是从高位开始,数据传输的格式为 8bit,数据采样从第二个时间边沿开始,空闲状态时,时钟线 clk 为高电平。 

    2.3 内部结构框架图

    2.4 W25Q64的标准SPI操作流程

    W25Q64标准SPI总线接口包含四个信号: 串行时钟(CLK)、片选端(/CS)、串行数据输入(DI)和串行数据输出(DO)

    DI输入引脚在CLK的上升沿连续写命令、地址或数据到芯片内。

    DO输出引脚在CLK的下降沿从芯片内读出数据或状态。

    W25Q64分别支持SPI总线工作模式0和工作模式3。模式0和模式3的主要区别在于常态时的CLK信号不同;对于模式0来说,当SPI主机已准备好数据还没传输到串行Flash中时,CLK信号常态为低;

    设备数据传输是从高位开始,数据传输的格式为8bit,数据采样从第二个时间边沿开始,空闲状态时,时钟线clk为高电平

     

    2.5 部分控制和状态寄存器介绍

    2.5.1 W25Q64的指令表

     

    指令名称

    字节 1

    (代码)

     

    字节 2

     

    字节 3

     

    字节 4

     

    字节 5

     

    字节 6

    写使能

    06h

    write_enabled 

    禁止写

    04h

     

    读状态寄存器 1

    05h

    (S7-S0)(2) 

     

    读状态寄存器 2

    35h

    (S15-S8)(2) 

     

    写状态寄存器

    01h

    (S7-S0)

    (S15-S8)

     

    页编程

    02h

    A23-A16

    A15-A8

    A7-A0

    (D7-D0)

     

    四倍页编程

    32h

    A23-A16

    A15-A8

    A7-A0

    (D7-D0,…)(3) 

     

    块擦除(64KB)

    D8h

    A23-A16

    A15-A8

    A7-A0

     

    块擦除(32KB)

    52h

    A23-A16

    A15-A8

    A7-A0

     

    扇区擦除(4KB)

    20h

    A23-A16

    A15-A8

    A7-A0

     

    全片擦除

    C7h/60h

     

    暂停擦除

    75h

     

    恢复擦除

    7Ah

     

    掉电模式

    B9h

     

    高性能模式

    A3h

     

     

     

     

     

    2.5.2 读状态寄存器1

    状态寄存器1的内部结构如下:

    状态寄存器1的S0位是当前W25Q64的忙状态;为1的时候表示设备正在执行程序(可能是在擦除芯片)或写状态寄存器指令,这个时候设备将忽略传来的指令, 除了读状态寄存器和擦除暂停指令外,其他写指令或写状态指令都无效S0 0 状态时指示设备已经执行完毕,可以进行下一步操作。 

    读状态寄存器1的时序如下:

    读取状态寄存器的指令是 8 位的指令。发送指令之前,先将/CS 拉低,再发送指令码“05 h” 或者“35h”。设备收到读取状态寄存器的指令后,将状态信息(高位)依次移位发送出去,读出的状态信息,最低位为 1 代表忙,最低位为 0 代表可以操作,状态信息读取完毕,将片选线拉高。 

    读状态寄存器指令可以使用在任何时候,即使程序在擦除的过程中或者写状态寄存器周期正在进行中。这可以检测忙碌状态来确定周期是否完成,以确定设备是否可以接受另一个指令。
     

    2.5.3 读制造商ID和芯片ID

     时序图如下:

    读取制造商/设备 ID 指令可以读取制造商 ID 和特定的设备 ID。读取之前,拉低 CS 片选信号,接着发送指令代码“90h” ,紧随其后的是一个 24 位地址(A23-A0)000000h。 设备收到指令之后,会发出华邦电子制造商 ID(EFh) 和设备ID(w25q64 为 16h)。如果 24 位地址设置为 000001h ,设备 ID 会先发出,然后跟着制造商 ID。制造商和设备ID可以连续读取。完成指令后,片选信号/ CS 拉高。

    2.5.4 全片擦除(C7h/60h)

    全芯片擦除指令,可以将整个芯片的所有内存数据擦除,恢复到 0XFF 状态。写入全芯片擦除指令之前必须执行设备写使能(发送设备写使能指令 0x06),并判断状态寄存器(状态寄存器位最低位必须等于 0 才能操作)。发送全芯片擦除指令前,先拉低/ CS,接着发送擦除指令码”C7h”或者是”60h”, 指令码发送完毕后,拉高片选线 CS/,,并判断状态位,等待擦除结束。全片擦除指令尽量少用,擦除会缩短设备的寿命。

    2.5.5 读数据(03h)

    读取数据指令允许按顺序读取一个字节的内存数据。当片选 CS/拉低之后,紧随其后是一个 24 位的地址(A23-A0)(需要发送 3 次,每次 8 个字节,先发高位)。芯片收到地址后,将要读的数据按字节大小转移出去,数据是先转移高位,对于单片机,时钟下降沿发送数据,上升沿接收数据。读数据时,地址会自动增加,允许连续的读取数据。这意味着读取整个内存的数据,只要用一个指令就可以读完。数据读取完成之后,片选信号/ CS 拉高。

    读取数据的指令序列,如上图所示。如果一个读数据指令而发出的时候,设备正在擦除扇区,或者(忙= 1),该读指令将被忽略,也不会对当前周期有什么影响。

    三、SPI时序介绍

    SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间。

    SPI是一种高速、高效率的串行接口技术,一共有4根线。通常由一个主模块和一个或多个从模块组成,主模块选择一个从模块进行同步通信,从而完成数据的交换。SPI是一个环形结构,通信时需要至少4根线(在单向传输时3根线也可以)。分别是MISO(主设备数据输入)、MOSI(主设备数据输出)、SCLK(时钟)、CS(片选)。
    (1)MISO– Master Input Slave Output,主设备数据输入,从设备数据输出;
    (2)MOSI– Master Output Slave Input,主设备数据输出,从设备数据输入;
    (3)SCLK – Serial Clock,时钟信号,由主设备产生;
    (4)CS – Chip Select,从设备使能信号,由主设备控制。

    其中,CS是从芯片是否被主芯片选中的控制信号,也就是说只有片选信号为预先规定的使能信号时(高电位或低电位),主芯片对此从芯片的操作才有效。这就使在同一条总线上连接多个SPI设备成为可能。接下来就负责通讯的3根线了。通讯是通过数据交换完成的,这里先要知道SPI是串行通讯协议,也就是说数据是一位一位的传输的。这就是SCLK时钟线存在的原因,由SCLK提供时钟脉冲,SDI,SDO则基于此脉冲完成数据传输。数据输出通过 SDO线,数据在时钟上升沿或下降沿时改变,在紧接着的下降沿或上升沿被读取。完成一位数据传输,输入也使用同样原理。因此,至少需要8次时钟信号的改变(上沿和下沿为一次),才能完成8位数据的传输。
    时钟信号线SCLK只能由主设备控制,从设备不能控制。这样的传输方式有一个优点,在数据位的传输过程中可以暂停,也就是时钟的周期可以为不等宽,因为时钟线由主设备控制,当没有时钟跳变时,从设备不采集或传送数据。SPI还是一个数据交换协议:因为SPI的数据输入和输出线独立,所以允许同时完成数据的输入和输出。芯片集成的SPI串行同步时钟极性和相位可以通过寄存器配置,IO模拟的SPI串行同步时钟需要根据从设备支持的时钟极性和相位来通讯。SPI通信原理比I2C要简单,IIC有应答机制,可以确保数据都全部发送成。SPI接口没有指定的流控制,没有应答机制确认是否接收到数据,速度上更加快。

    SPI总线通过时钟极性和相位可以配置成4种时序:

    STM32F103参考手册,SPI章节介绍的时序图:

    SPI时序比较简单,CPU如果没有硬件支持,可以直接写代码采用IO口模拟,下面是模拟时序的示例的代码:

    SPI的模式1:
    u8 SPI_ReadWriteOneByte(u8 tx_data)
    {
    	u8 i,rx_data=0;
    	SCK=0; //空闲电平(默认初始化情况)
    	for(i=0;i<8;i++)
    	{
    		/*1. 主机发送一位数据*/
    		SCK=0;//告诉从机,主机将要发送数据
    		if(tx_data&0x80)MOSI=1; //发送数据
    		else MOSI=0;
    		SCK=1; //告诉从机,主机数据发送完毕
    		tx_data<<=1; //继续发送下一位
    		
    		/*2. 主机接收一位数据*/
    		rx_data<<=1; //默认认为接收到0
    		if(MISO)rx_data|=0x01;
    	}
    	SCK=0; //恢复空闲电平
    	return rx_data;
    }
    
    SPI的模式2:
    u8 SPI_ReadWriteOneByte(u8 tx_data)
    {
    	u8 i,rx_data=0;
    	SCK=0; //空闲电平(默认初始化情况)
    	for(i=0;i<8;i++)
    	{
    		/*1. 主机发送一位数据*/
    		SCK=1;//告诉从机,主机将要发送数据
    		if(tx_data&0x80)MOSI=1; //发送数据
    		else MOSI=0;
    		SCK=0; //告诉从机,主机数据发送完毕
    		tx_data<<=1; //继续发送下一位
    		
    		/*2. 主机接收一位数据*/
    		rx_data<<=1; //默认认为接收到0
    		if(MISO)rx_data|=0x01;
    	}
    	SCK=0; //恢复空闲电平
    	return rx_data;
    }
    
    
    SPI的模式3:
    u8 SPI_ReadWriteOneByte(u8 tx_data)
    {
    	u8 i,rx_data=0;
    	SCK=1; //空闲电平(默认初始化情况)
    	for(i=0;i<8;i++)
    	{
    		/*1. 主机发送一位数据*/
    		SCK=1;//告诉从机,主机将要发送数据
    		if(tx_data&0x80)MOSI=1; //发送数据
    		else MOSI=0;
    		SCK=0; //告诉从机,主机数据发送完毕
    		tx_data<<=1; //继续发送下一位
    		
    		/*2. 主机接收一位数据*/
    		rx_data<<=1; //默认认为接收到0
    		if(MISO)rx_data|=0x01;
    	}
    	SCK=1; //恢复空闲电平
    	return rx_data;
    }
    
    SPI的模式4:
    u8 SPI_ReadWriteOneByte(u8 tx_data)
    {
    	u8 i,rx_data=0;
    	SCK=1; //空闲电平(默认初始化情况)
    	for(i=0;i<8;i++)
    	{
    		/*1. 主机发送一位数据*/
    		SCK=0;//告诉从机,主机将要发送数据
    		if(tx_data&0x80)MOSI=1; //发送数据
    		else MOSI=0;
    		SCK=1; //告诉从机,主机数据发送完毕
    		tx_data<<=1; //继续发送下一位
    		
    		/*2. 主机接收一位数据*/
    		rx_data<<=1; //默认认为接收到0
    		if(MISO)rx_data|=0x01;
    	}
    	SCK=1; //恢复空闲电平
    	return rx_data;
    }

    四、W25Q64的示例代码

    4.1 STM32采用硬件SPI读写W25Q64示例代码

    /*
    函数功能:SPI初始化(模拟SPI)
    硬件连接:
    MISO--->PB14
    MOSI--->PB15
    SCLK--->PB13
    */
    void SPI_Init(void)
    {
    	/*开启时钟*/
    	RCC->APB1ENR|=1<<14;   //开启SPI2时钟
    	RCC->APB2ENR|=1<<3;    //PB
    	GPIOB->CRH&=0X000FFFFF; //清除寄存器
    	GPIOB->CRH|=0XB8B00000;
    	GPIOB->ODR|=0X7<<13;    	//PB13/14/15上拉--输出高电平
    	/*SPI2基本配置*/
    	SPI2->CR1=0X0; 		//清空寄存器
    	SPI2->CR1|=0<<15; //选择“双线双向”模式
    	SPI2->CR1|=0<<11; //使用8位数据帧格式进行发送/接收;
    	SPI2->CR1|=0<<10; //全双工(发送和接收);
    	SPI2->CR1|=1<<9;  //启用软件从设备管理
    	SPI2->CR1|=1<<8;  //NSS
    	SPI2->CR1|=0<<7;  //帧格式,先发送高位
    	SPI2->CR1|=0x0<<3;//当总线频率为36MHZ时,SPI速度为18MHZ,高速。
    	SPI2->CR1|=1<<2;  //配置为主设备
    	SPI2->CR1|=1<<1;  //空闲状态时, SCK保持高电平。
    	SPI2->CR1|=1<<0;  //数据采样从第二个时钟边沿开始。
    	SPI2->CR1|=1<<6;  //开启SPI设备。
    }
    
    
    /*
    函数功能:SPI读写一个字节
    */
    u8 SPI_ReadWriteOneByte(u8 data_tx)
    {
        u16 cnt=0;				 
        while((SPI2->SR&1<<1)==0)		 //等待发送区空--等待发送缓冲为空	
        {
          cnt++;
          if(cnt>=65530)return 0; 	  //超时退出  u16=2个字节
        }	
        SPI2->DR=data_tx;	 	  		      //发送一个byte 
        cnt=0;
        while((SPI2->SR&1<<0)==0) 		//等待接收完一个byte   
        {
          cnt++;
          if(cnt>=65530)return 0;	   //超时退出
        }	  						    
        return SPI2->DR;          		//返回收到的数据	
    }
    
    
    /*
    函数功能:W25Q64初始化
    硬件连接:
    MOSI--->PB15
    MISO--->PB14
    SCLK--->PB13
    CS----->PB12
    */
    void W25Q64_Init(void)
    {
    	/*1. 开时钟*/
    	RCC->APB2ENR|=1<<3; //PB
    	
    	/*2. 配置GPIO口模式*/
    	GPIOB->CRH&=0xFFF0FFFF;
    	GPIOB->CRH|=0x00030000;
    	
    	W25Q64_CS=1; //未选中芯片
    	SPI_Init();   //SPI初始化
    }
    
    
    /*
    函数功能:读取芯片的ID号
    */
    u16 W25Q64_ReadID(void)
    {
    	u16 id;
    	/*1. 拉低片选*/
    	W25Q64_CS=0;
    	
    	/*2. 发送读取ID的指令*/
    	SPI_ReadWriteOneByte(0x90);
    	
    	/*3. 发送24位的地址-0*/
    	SPI_ReadWriteOneByte(0);
    	SPI_ReadWriteOneByte(0);
    	SPI_ReadWriteOneByte(0);
    	
    	/*4. 读取芯片的ID*/
    	id=SPI_ReadWriteOneByte(0xFF)<<8;
    	id|=SPI_ReadWriteOneByte(0xFF);
    
    	/*5. 拉高片选*/
    	W25Q64_CS=1;
    	return id;
    }
    
    /*
    函数功能:检测W25Q64状态
    */
    void W25Q64_CheckStat(void)
    {
    	u8 stat=1;
    	while(stat&1<<0)
    	{
    		W25Q64_CS=0; //选中芯片
    		SPI_ReadWriteOneByte(0x05);      //发送读状态寄存器1指令
    		stat=SPI_ReadWriteOneByte(0xFF); //读取状态
    		W25Q64_CS=1; //取消选中芯片
    	}
    }
    
    
    /*
    函数功能:页编程
    说    明:一页最多写256个字节。 写数据之前,必须保证空间是0xFF
    函数参数:
    u32 addr:页编程起始地址
    u8 *buff:写入的数据缓冲区
    u16 len :写入的字节长度
    */
    void W25Q64_PageWrite(u32 addr,u8 *buff,u16 len)
    {
    	u16 i;
    	W25Q64_Enabled();  						//写使能
    	W25Q64_CS=0; //选中芯片
    	SPI_ReadWriteOneByte(0x02); //页编程指令
    	SPI_ReadWriteOneByte(addr>>16); //24~16地址
    	SPI_ReadWriteOneByte(addr>>8);  //16~8地址
    	SPI_ReadWriteOneByte(addr);     //8~0地址
    
    	for(i=0;i<len;i++)
    	{
    		SPI_ReadWriteOneByte(buff[i]);     //8~0地址	
    	}
    	W25Q64_CS=1; //取消选中芯片
    	W25Q64_CheckStat();  //检测芯片忙状态
    }
    
    
    /*
    函数功能:连续读数据
    函数参数:
    u32 addr:读取数据的起始地址
    u8 *buff:读取数据存放的缓冲区
    u32 len :读取字节的长度
    */
    void W25Q64_ReadByteData(u32 addr,u8 *buff,u32 len)
    {
    	u32 i;
    	W25Q64_CS=0; //选中芯片
    	SPI_ReadWriteOneByte(0x03);     //读数据指令
    	SPI_ReadWriteOneByte(addr>>16); //24~16地址
    	SPI_ReadWriteOneByte(addr>>8);  //16~8地址
    	SPI_ReadWriteOneByte(addr);     //8~0地址
    	for(i=0;i<len;i++)buff[i]=SPI_ReadWriteOneByte(0xFF);
    	W25Q64_CS=1; //取消选中芯片
    }
    
    
    /*
    函数功能:擦除一个扇区
    函数参数:
    u32 addr:擦除扇区的地址范围
    */
    void W25Q64_ClearSector(u32 addr)
    {
    	W25Q64_Enabled();  						//写使能
    	W25Q64_CS=0; //选中芯片
    	SPI_ReadWriteOneByte(0x20);     //扇区擦除指令
    	SPI_ReadWriteOneByte(addr>>16); //24~16地址
    	SPI_ReadWriteOneByte(addr>>8);  //16~8地址
    	SPI_ReadWriteOneByte(addr);     //8~0地址
    	W25Q64_CS=1; 				//取消选中芯片
    	W25Q64_CheckStat();  //检测芯片忙状态
    }
    
    /*
    函数功能:写使能
    */
    void W25Q64_Enabled(void)
    {
    	W25Q64_CS=0; //选中芯片
    	SPI_ReadWriteOneByte(0x06);     //写使能
    	W25Q64_CS=1; //取消选中芯片
    }
    
    
    /*
    函数功能:指定位置写入指定个数的数据,不考虑擦除问题
    注意事项:W25Q64只能将1写为,不能将0写为1。
    函数参数:
    u32 addr---写入数据的起始地址
    u8 *buff---写入的数据
    u32 len---长度
    */
    void W25Q64_WriteByteDataNoCheck(u32 addr,u8 *buff,u32 len)
    {
    	u32 page_remain=256-addr%256; //计算当前页还可以写下多少数据
    	if(len<=page_remain) //如果当前写入的字节长度小于剩余的长度
    	{
    		page_remain=len;
    	}
    	while(1)
    	{
    		W25Q64_PageWrite(addr,buff,page_remain);
    		if(page_remain==len)break; //表明数据已经写入完毕
    		buff+=page_remain; //buff向后偏移地址
    		addr+=page_remain; //起始地址向后偏移
    		len-=page_remain;  //减去已经写入的字节数
    		if(len>256)page_remain=256;  //如果大于一页,每次就直接写256字节
    		else page_remain=len;
    	}
    }
    
    
    /*
    函数功能:指定位置写入指定个数的数据,考虑擦除问题,完善代码
    函数参数:
    u32 addr---写入数据的起始地址
    u8 *buff---写入的数据
    u32 len---长度
    说明:擦除的最小单位扇区,4096字节
    */
    static u8 W25Q64_READ_WRITE_CHECK_BUFF[4096];
    void W25Q64_WriteByteData(u32 addr,u8 *buff,u32 len)
    {
        u32 i;
        u32 len_w;
        u32 sector_addr; //存放扇区的地址
        u32 sector_move; //扇区向后偏移的地址
        u32 sector_size; //扇区大小。(剩余的空间大小)
        u8 *p=W25Q64_READ_WRITE_CHECK_BUFF;//存放指针
        sector_addr=addr/4096; //传入的地址是处于第几个扇区
        sector_move=addr%4096; //计算传入的地址存于当前的扇区的偏移量位置
        sector_size=4096-sector_move; //得到当前扇区剩余的空间
    
        if(len<=sector_size)
        {
                sector_size=len; //判断第一种可能性、一次可以写完
        }
        
        while(1)
        {
            W25Q64_ReadByteData(addr,p,sector_size);	 //读取剩余扇区里的数据
            for(i=0;i<sector_size;i++)
            {
                if(p[i]!=0xFF)break;
            }
            if(i!=sector_size)  //判断是否需要擦除
            {
                 W25Q64_ClearSector(sector_addr*4096);
            }
    //        for(i=0;i<len;i++)
    //        {
    //             W25Q64_READ_WRITE_CHECK_BUFF[i]=buff[len_w++]; 
            }
    //        W25Q64_WriteByteDataNoCheck(addr,W25Q64_READ_WRITE_CHECK_BUFF,sector_size);
            W25Q64_WriteByteDataNoCheck(addr,buff,sector_size);
            if(sector_size==len)break;
    
            addr+=sector_size; //向后偏移地址
            buff+=sector_size ;//向后偏移
            len-=sector_size;  //减去已经写入的数据
            sector_addr++;     //校验第下个扇区
            if(len>4096)       //表明还可以写一个扇区
            {
                    sector_size=4096;//继续写一个扇区
            }
            else
            {
                    sector_size=len; //剩余的空间可以写完
            }
        }
    }
    

    4.2 STM32采用硬件SPI读写W25Q64示例代码

    #include "spi.h"
    
    
    /*
    函数功能:SPI初始化(模拟SPI)
    硬件连接:
    MISO--->PB14
    MOSI--->PB15
    SCLK--->PB13
    */
    void SPI_Init(void)
    {
    	/*1. 开时钟*/
    	RCC->APB2ENR|=1<<3; //PB
    
    	/*2. 配置GPIO口模式*/
    	GPIOB->CRH&=0x000FFFFF;
    	GPIOB->CRH|=0x38300000;
    
    	/*3. 上拉*/
    	SPI_MOSI=1;
    	SPI_MISO=1;
    	SPI_SCLK=1;
    }
    
    /*
    函数功能:SPI读写一个字节
    */
    u8 SPI_ReadWriteOneByte(u8 data_tx)
    {
    	u8 data_rx=0; //存放读取的数据
    	u8 i;
    	for(i=0;i<8;i++)
    	{
    		SPI_SCLK=0; //准备发送数据
    		if(data_tx&0x80)SPI_MOSI=1;
    		else SPI_MOSI=0;
    		data_tx<<=1; //依次发送最高位
    		SPI_SCLK=1;  //表示主机数据发送完成,表示从机发送完毕
    		
    		data_rx<<=1; //表示默认接收的是0
    		if(SPI_MISO)data_rx|=0x01;
    	}
    	return data_rx;
    }
    
    #include "W25Q64.h"
    
    /*
    函数功能:W25Q64初始化
    硬件连接:
    MOSI--->PB15
    MISO--->PB14
    SCLK--->PB13
    CS----->PB12
    */
    void W25Q64_Init(void)
    {
    	/*1. 开时钟*/
    	RCC->APB2ENR|=1<<3; //PB
    	
    	/*2. 配置GPIO口模式*/
    	GPIOB->CRH&=0xFFF0FFFF;
    	GPIOB->CRH|=0x00030000;
    	
    	W25Q64_CS=1; //未选中芯片
    	SPI_Init();   //SPI初始化
    }
    
    
    /*
    函数功能:读取芯片的ID号
    */
    u16 W25Q64_ReadID(void)
    {
    	u16 id;
    	/*1. 拉低片选*/
    	W25Q64_CS=0;
    
    	/*2. 发送读取ID的指令*/
    	SPI_ReadWriteOneByte(0x90);
    
    	/*3. 发送24位的地址-0*/
    	SPI_ReadWriteOneByte(0);
    	SPI_ReadWriteOneByte(0);
    	SPI_ReadWriteOneByte(0);
    
    	/*4. 读取芯片的ID*/
    	id=SPI_ReadWriteOneByte(0xFF)<<8;
    	id|=SPI_ReadWriteOneByte(0xFF);
    
    	/*5. 拉高片选*/
    	W25Q64_CS=1;
    	return id;
    }
    
    /*
    函数功能:检测W25Q64状态
    */
    void W25Q64_CheckStat(void)
    {
    	u8 stat=1;
    	while(stat&1<<0)
    	{
    		W25Q64_CS=0; //选中芯片
    		SPI_ReadWriteOneByte(0x05);      //发送读状态寄存器1指令
    		stat=SPI_ReadWriteOneByte(0xFF); //读取状态
    		W25Q64_CS=1; //取消选中芯片
    	}
    }
    
    
    /*
    函数功能:页编程
    说    明:一页最多写256个字节。 写数据之前,必须保证空间是0xFF
    函数参数:
    u32 addr:页编程起始地址
    u8 *buff:写入的数据缓冲区
    u16 len :写入的字节长度
    */
    void W25Q64_PageWrite(u32 addr,u8 *buff,u16 len)
    {
    	u16 i;
    	W25Q64_Enabled();  						//写使能
    	W25Q64_CS=0; //选中芯片
    	SPI_ReadWriteOneByte(0x02); //页编程指令
    	SPI_ReadWriteOneByte(addr>>16); //24~16地址
    	SPI_ReadWriteOneByte(addr>>8);  //16~8地址
    	SPI_ReadWriteOneByte(addr);     //8~0地址
    
    	for(i=0;i<len;i++)
    	{
    		SPI_ReadWriteOneByte(buff[i]);     //8~0地址	
    	}
    	W25Q64_CS=1; //取消选中芯片
    	W25Q64_CheckStat();  //检测芯片忙状态
    }
    
    
    /*
    函数功能:连续读数据
    函数参数:
    u32 addr:读取数据的起始地址
    u8 *buff:读取数据存放的缓冲区
    u32 len :读取字节的长度
    */
    void W25Q64_ReadByteData(u32 addr,u8 *buff,u32 len)
    {
    	u32 i;
    	W25Q64_CS=0; //选中芯片
    	SPI_ReadWriteOneByte(0x03);     //读数据指令
    	SPI_ReadWriteOneByte(addr>>16); //24~16地址
    	SPI_ReadWriteOneByte(addr>>8);  //16~8地址
    	SPI_ReadWriteOneByte(addr);     //8~0地址
    	for(i=0;i<len;i++)buff[i]=SPI_ReadWriteOneByte(0xFF);
    	W25Q64_CS=1; //取消选中芯片
    }
    
    
    /*
    函数功能:擦除一个扇区
    函数参数:
    				u32 addr:擦除扇区的地址范围
    */
    void W25Q64_ClearSector(u32 addr)
    {
    	W25Q64_Enabled();  						//写使能
    	W25Q64_CS=0; //选中芯片
    	SPI_ReadWriteOneByte(0x20);     //扇区擦除指令
    	SPI_ReadWriteOneByte(addr>>16); //24~16地址
    	SPI_ReadWriteOneByte(addr>>8);  //16~8地址
    	SPI_ReadWriteOneByte(addr);     //8~0地址
    	W25Q64_CS=1; 				//取消选中芯片
    	W25Q64_CheckStat();  //检测芯片忙状态
    }
    
    /*
    函数功能:写使能
    */
    void W25Q64_Enabled(void)
    {
    	W25Q64_CS=0; //选中芯片
    	SPI_ReadWriteOneByte(0x06);     //写使能
    	W25Q64_CS=1; //取消选中芯片
    }
    
    
    /*
    函数功能:指定位置写入指定个数的数据,不考虑擦除问题
    注意事项:W25Q64只能将1写为,不能将0写为1。
    函数参数:
    u32 addr---写入数据的起始地址
    u8 *buff---写入的数据
    u32 len---长度
    */
    void W25Q64_WriteByteDataNoCheck(u32 addr,u8 *buff,u32 len)
    {
    	u32 page_remain=256-addr%256; //计算当前页还可以写下多少数据
    	if(len<=page_remain) //如果当前写入的字节长度小于剩余的长度
    	{
    		page_remain=len;
    	}
    	while(1)
    	{
    		W25Q64_PageWrite(addr,buff,page_remain);
    		if(page_remain==len)break; //表明数据已经写入完毕
    		buff+=page_remain; //buff向后偏移地址
    		addr+=page_remain; //起始地址向后偏移
    		len-=page_remain;  //减去已经写入的字节数
    		if(len>256)page_remain=256;  //如果大于一页,每次就直接写256字节
    		else page_remain=len;
    	}
    }
    
    
    /*
    函数功能:指定位置写入指定个数的数据,考虑擦除问题,完善代码
    函数参数:
    u32 addr---写入数据的起始地址
    u8 *buff---写入的数据
    u32 len---长度
    说明:擦除的最小单位扇区,4096字节
    */
    static u8 W25Q64_READ_WRITE_CHECK_BUFF[4096];
    void W25Q64_WriteByteData(u32 addr,u8 *buff,u32 len)
    {
    	u32 i;
    	u32 sector_addr; //存放扇区的地址
    	u32 sector_move; //扇区向后偏移的地址
    	u32 sector_size; //扇区大小。(剩余的空间大小)
    	u8 *p=W25Q64_READ_WRITE_CHECK_BUFF;//存放指针
    	sector_addr=addr/4096; //传入的地址是处于第几个扇区
    	sector_move=addr%4096; //计算传入的地址存于当前的扇区的偏移量位置
    	sector_size=4096-sector_move; //得到当前扇区剩余的空间
    
    	if(len<=sector_size)
    	{
    		sector_size=len; //判断第一种可能性、一次可以写完
    	}
    
    	while(1)
    	{
    		W25Q64_ReadByteData(addr,p,sector_size);	 //读取剩余扇区里的数据
    		for(i=0;i<sector_size;i++)
    		{
    			if(p[i]!=0xFF)break;
    		}
    		if(i!=sector_size)  //判断是否需要擦除
    		{
    			W25Q64_ClearSector(sector_addr*4096);
    		}
    		W25Q64_WriteByteDataNoCheck(addr,buff,sector_size);
    		if(sector_size==len)break;
    
    		addr+=sector_size; //向后偏移地址
    		buff+=sector_size ;//向后偏移
    		len-=sector_size;  //减去已经写入的数据
    		sector_addr++;     //校验第下个扇区
    		if(len>4096)       //表明还可以写一个扇区
    		{
    			sector_size=4096;//继续写一个扇区
    		}
    		else
    		{
    			sector_size=len; //剩余的空间可以写完
    		}
    	}
    }
    

     

    展开全文
  • 03 Q-Learning介绍 Q-Learning是Value-Based的强化学习算法,所以算法里面有一个非常重要的Value就是Q-Value,也是Q-Learning叫法的由来。这里重新把强化学习的五个基本部分介绍一下。 Agent(智能体): 强化学习...

     Datawhale干货 

    作者:知乎King James,伦敦国王大学

    知乎 | https://www.zhihu.com/people/xu-xiu-jian-33

    前言上篇介绍了什么是强化学习,应大家需求,本篇实战讲解强化学习,所有的实战代码可以自行下载运行。

    本篇使用强化学习领域经典的Project-Pacman项目进行实操,Python2.7环境,使用Q-Learning算法进行训练学习,将讲解强化学习实操过程中的各处细节。如何设置Reward函数,如何更新各(State,Action)下的Q-Value值等。有基础的读者可以直接看Part4实战部分。文章略长,细节讲解很多,适合新手入门强化学习。

    01 强化学习

    关于强化学习的基础介绍,可以阅读我上一篇帖子,本篇不再介绍。如果完全是零基础的读者,建议先阅读上一篇文章。里面介绍了强化学习的五大基本组成部分、训练过程、各大常见算法以及实际工业界应用等。

    02 Pacman Project讲解

    Pacman-吃豆人游戏,本身是上世纪80年代日本南梦宫游戏公司推出的一款街机游戏,在当时风靡大街小巷。后来加州大学伯克利分校,这所只有诺贝尔奖获得者才配在学校里面拥有固定车位的顶级公立大学,将Pacman游戏引进到强化学习的课程中,作为实操项目。慢慢地成为该领域的经典Project。

    项目链接:http://ai.berkeley.edu/project_overview.html,这个项目因为时间比较久,所以整体是Python2.7的源码,没有最新的Python3源码。

    d4264f1288fe2f899ec7c2a4715fafa8.png

    Pacman游戏目标很简单,就是Agent要把屏幕里面所有的豆子全部吃完,同时又不能被幽灵碰到,被幽灵碰到则游戏结束,幽灵也是在不停移动的。Agent每走一步、每吃一个豆子或者被幽灵碰到,屏幕左上方这分数都会发生变化,图例中当前分数是435分。

    本次项目,我们基于Q-Learning算法,让Pacman先自行探索训练2000次。探索训练结束后,重新让Pacman运行10次,测试这10次中Pacman成功吃完所有屏幕中所有豆子的次数,10次中至少成功8次才算有效。

    03 Q-Learning介绍

    Q-Learning是Value-Based的强化学习算法,所以算法里面有一个非常重要的Value就是Q-Value,也是Q-Learning叫法的由来。这里重新把强化学习的五个基本部分介绍一下。

    • Agent(智能体): 强化学习训练的主体就是Agent:智能体。Pacman中就是这个张开大嘴的黄色扇形移动体。

    • Environment(环境): 整个游戏的大背景就是环境;Pacman中Agent、Ghost、豆子以及里面各个隔离板块组成了整个环境。

    • State(状态): 当前 Environment和Agent所处的状态,因为Ghost一直在移动,豆子数目也在不停变化,Agent的位置也在不停变化,所以整个State处于变化中;State包含了Agent和Environment的状态。

    • Action(行动): 基于当前的State,Agent可以采取哪些action,比如向左or右,向上or下;Action是和State强挂钩的,比如上图中很多位置都是有隔板的,很明显Agent在此State下是不能往左或者往右的,只能上下;

    • Reward(奖励): Agent在当前State下,采取了某个特定的action后,会获得环境的一定反馈就是Reward。这里面用Reward进行统称,虽然Reward翻译成中文是“奖励”的意思,但其实强化学习中Reward只是代表环境给予的“反馈”,可能是奖励也可能是惩罚。比如Pacman游戏中,Agent碰见了Ghost那环境给予的就是惩罚。

    本次项目我们使用Q-Learning,所以在五个基本部分之外多了一个Q-Value。

    • Q-Value(State, Action): Q-value是由State和Action组合在一起决定的,这里的Value不是Reward,Reward是Value组成的一部分,具体如何生成Q-value下面会单独介绍。实际的项目中我们会存储一张表,我们叫它Q表。key是(state, action), value就是对应的Q-value。每当agent进入到某个state下时,我们就会来这张表进行查询,选择当前State下对应Value最大的Action,执行这个action进入到下一个state,然后继续查表选择action,这样循环。Q-Value的价值就在于指导Agent在不同state下选择哪个action。

    重点来了!!!如何知道整个训练过程中,Agent会遇到哪些State,每个State下面可以采取哪些Action。最最重要的是,如何将每个(State, Action)对应的Q-value从训练中学习出来?

    3.1 Bellman 方程

    首先我们介绍一下贝尔曼方程,它是我们接下来介绍如何更新学习出Q-Value值的前提。贝尔曼方程是由美国一位叫做理查德-贝尔曼科学家发现并提出的。它的核心思想是:当我们在特定时间点和状态下去考虑下一步的决策,我们不仅仅要关注当前决策立即产生的Reward,同时也要考虑当前的决策衍生产生未来持续性的Reward。简单地说就是既要考虑当前收益最大化,还需要去关注未来持续的收益。

    3.2 贝尔曼方程的Q-Value版

    介绍完贝尔曼方程的思想后,在Q-learning算法中如何去更新Q-Value?

    cefef007a049bf4013b468c56fa32b51.png

    如上图的表达式,我们更新Q(s,a)时不仅关注当前收益也关注未来收益,当前收益就是状态变更环境立即反馈的reward,未来收益就是状态变更后新状态对应可以采取的action中最大的Value,同时乘以折扣率γ。对机器学习不够了解的,可能比较难以理解为什么要加一个学习率和折扣率,意义是什么?简单来说学习率和折扣率的设置是希望学习更新过程缓慢一些,不希望某一步的学习跨度过大,从而对整个的学习结果造成比较大的偏差。因为Q(s,a)会更新迭代很多次,不能因为某一次的学习对最终的Q-value产生非常大的影响。

    下图是简版的Q-Learning算法阐述:

    f48f31b8d3308d1bbf34dbe164289a60.png

    04 Q-Learning实战

    介绍完Q-Learning算法正式进入Python实战,下面是实战用的代码压缩包。(有需要的伙伴可公众号后台回复日期「20211212」下载)

    8728027ee8227f76bef9b0484832b2de.png

    4.1 预热

    首先我们先感受一下Pacman这个游戏,通过人工指挥和随机运动,观察一下整个实验的效果。

    手动指挥: 首先进入该文件的工作路径,cd /Users/账户名/Desktop/pacman,执行命令:python pacman.py ;就会看到下图,通过电脑键盘的上下左右键,就可以指挥Pacman行动了。

    e8dbe9322ef1c0afc18730fc2d4db89e.png

    随机行动: 在刚刚的工作路径下执行该命令:python pacman.py -p RandomAgent -n 1

    我们让 Pacman采取随机策略玩一遍游戏。

    4.2 Q-Learning算法训练

    现在我们使用Q-Learning算法来训练Pacman,本次Project编写的代码都在mlLearningAgents.py文件中,我们在该文件里面编写代码。

    (1)整体思路

    因为本次Pacman Project项目中我们重点在于应用Q-learning算法去进行训练,指导Agent行动。所以项目中有很多其他现成的接口我们都是直接用的。比如

    • State:game.py文件已经将如何获取Agent当前的State定义成了Class,直接引用即可;

    • Action:game.py文件已经将Agent在当前State可以采取的Action定义成了Class,直接引用即可;(感兴趣的同学可以自己打开文件查看代码,核心就是建立一个坐标系,然后确定挡板、Ghost、豆子、Agent的位置,然后进行判断和数学表达。)

    • 参数设置:学习率alpha我们设置为0.2,折扣率gamma设置为0.8,最终训练完我们让Pacman运行numTraining=10次查看效果,同时这里面有一个探索率epsilon = 0.05。这就是上一篇介绍的EE问题,我们不能光让Agent去执行Q-value最大的action,同时我们也需要让Pacman有一定的探索。

    651c90f4f86408f7e3929e4469af7490.png

    (2) Q-value表

    因为最开始我们无从得知Pacman会经历哪些状态State,以及采取哪些Action,所以我们最开始设置一个Q-value的空表,将训练中Pacman经历过的状态State,以及执行的Action,及最终学习产出的Q-value都保存在此张表里。

    7fe679cf41189572810c2d180070af2e.png

    (3) 选择Best Move

    定义一个Class,在State确认的情况下,首先找到最大的Q-value是多少。

    2ddd3ab3e8194be69eac275f4235707f.png

    然后找到该Q-value对应的Action:

    fd0f6ab9be0ebc7fd1e0c75f9a5cbe6c.png

    (GetLegalPAcmanActions即为获取当前state下,Agent可以执行的所有合理的action的类,文件里现成的类)

    (4) Update Q-value

    以下为更新Q-value的表达式,这里面有一个非常细节的地方。Pacman最开始运动的第一步的时候,我们是无法知晓下一步的State是什么的,源文件提供的类只能获取当前的State。但当Pacman从当前State倒推,我们是知道上一个State是什么的,因为Pacman刚刚经历过,只要我们将上一个State记录下来即可。所以整个的更新表达式逻辑变为了用当前的State去更新上一步的(State,Action)对应的Q-value。该思路是解决本问题的关键。

    cbd79e4ecc036f68dce8cd17fc75d1cb.png

    (5) 让Agent运动起来

    最后就是指导Pacman行动了,这里面存在大量的状态和动作的记录,我们需要将每一步经历的State和采取的Action都保存进对应的Table中。同时这里面还有一个细节,就是Reward的设置,一直没有提到从一个State变为另一个State如何设置Reward。Pacman项目中,我们可以取巧的使用项目中现有的Pacman每行动一步Score发生的变化作为Reward,两个状态变化时Score的差值我们认为就是Reward,这一步为我们节省了大量设置Reward的功夫。如果想自己设置Reward逻辑就是Pacman采取的行动离豆子越近Reward越多,离Ghost越近Reward越少的

    62ca1a1b981154af761305e11a1129e0.png

    训练时Pacman行动的策略一部分是探索时的Random choice,一部分是利用时的Best Move。探索比例前面参数设置已经提到0.05。以上Print的部分,是训练时我们打印每一步的训练结果。

    最后训练完以后,我们打印一条message,进行一下标记

    4bd2f9793a0a0faf2ecc654b78c31a53.png

    正式训练我们还是在之前的工作路径下执行如下命令:

    python pacman.py -p QLearnAgent -x 2000 -n 2010 -l smallGrid

    这里面QLearnAgent就是调用我们此次算法的类,2000次是训练次数,2010-2000=10,10即为我们测试的次数。smallGrid是我们此次测试和训练仅使用Pacman的小界面,不使用大的界面进行训练,因为大界面State太多,训练时间过长,为了更快完成实验看到效果我们在小的游戏界面上进行实验。
    下图是训练时打印的message;

    4c520b358858f5637adf637d9aa0cbed.png

    下图就是最终测试时Pacman的运行小界面smallgrid

    f0fa918ad6325c27e38ef74bd53d81c8.png

    最后这条就是统计的10次中,Pacman赢的次数,我们可以看到有8次win,所以圆满地完成了我们的实验。

    9f976d40976044987759073aa3736193.png

    2000次训练对于SmallGrid界面是差不多的,大家也可以将SmallGrid改为MediumGrid,但是要大幅提升训练次数,不然学习效果不明显。大家自己跑起来吧~

    7425ab4e59f968b0d4c6c2601f8afb95.png

    King James

    伦敦国王学院 数据科学硕士

    知乎同名

    36d5be0e3414b5d0507a2a8ae18f3c10.png

    整理不易,三连

    展开全文
  • Q学习(Q-learning)入门小例子及python实现

    万次阅读 多人点赞 2019-09-24 18:13:09
    一、从马尔科夫过程到Q学习 # 有一定基础的读者可以直接看第二部分 Q学习(Q-learning)算法是一种与模型无关的强化学习算法,以马尔科夫决策过程(Markov Decision Processes, MDPs)为理论基础。 标准的...

    一、从马尔科夫过程到Q学习

    # 有一定基础的读者可以直接看第二部分

    Q学习(Q-learning)算法是一种与模型无关的强化学习算法,以马尔科夫决策过程(Markov Decision Processes, MDPs)为理论基础。

    标准的马尔科夫决策过程可以用一个五元组<S,A,P,R,γ> 表示,其中:

    • S是一个离散有界的状态空间;
    • A是一个离散的动作空间; 
    • P为状态转移概率函数,表示agent在状态s下选取动作a后转移到a'的概率;
    • R为回报函数,用于计算agent由当前状态 选取动作 后转移到下一状态 得到的立即回报值,由当前状态和选取的动作决定,体现了马尔科夫性的特点;
    • γ是折扣因子,用于确定延迟回报与立即回报的相对比例, 越大表明延迟回报的重要程度越高。

    马尔科夫决策问题的目标是找到一个策略 ,使其回报函数 的长期累积值的数学期望

     

    最大。其中,策略π只和状态相关,与时间无关(静态的)。  是t时刻的环境状态, t时刻选择的动作。

    根据Bellman最优准则,得到最优策略 对应的最优指标为:

    其中, R(s,a)为r(st,at)的数学期望, 为在状态s下选取动作a后转移到下一状态状态s'的概率。由于某些环境中状态之间的转移概率P不容易获得,直接学习 是很困难的,而Q学习不需要获取转移概率P,因而可用来解决此类具有马尔科夫性的问题。

    Q学习是一种与环境无关的算法,是一种基于数值迭代的动态规划方法。定义一个Q函数作为评估函数:

    评估函数Q(s,a)的函数值是从状态s开始选择第一个动作a执行后获得的最大累积回报的折算值,通俗地说,Q值等于立即回报值r(s,a) 加上遵循最优策略的折算值,此时的最优策略可改写为:

    该策略表达式的意义在于:如果agent用Q函数代替 函数,就可以不考虑转移概率P,只考虑当前状态s的所有可供选择的动作a,并从中选出使Q(s,a)最大的动作,即agent对当前状态的部分Q值做出多次反应,便可以选出动作序列,使全局最优化。

    在Q学习中,agent由初始状态转移到目标状态的过程称为“Episode”,即“场景”。Q函数可以表示为以下的迭代形式进行Q矩阵的更新:

     

    在每一步的迭代中,上式又可写为:

     

    Q矩阵(st,at)位置元素的值等于回报函数R的相应值加上折扣因子γ乘以转换到下一个状态后最大的Q值。

    上述的Q学习算法可以看出,当划分的状态有限时,每一场景开始时随机选择的初始状态s在算法的指导下探索环境,最终一定可以到达目标状态s*,回报函数R(s,a)是有界的,并且动作的选择能够使每个状态映射到动作对的访问是无限频率,则整个学习过程就能够训练出来。

    Q学习通过对环境的不断探索,积累历史经验,agent通过不断试错来强化自身,最终可以达到自主选择最优动作的目标,即不论出于何种状态,都可给出到达目标状态的最优选择路径,该算法中环境和动作相互影响,动作的选择影响环境状态,环境也可以通过强化回报函数 来反馈动作的优劣性,影响动作的选择。

    参考文献:

    [1]汪黎明.制造企业零库存管理物资调度方法研究[J].价值工程, 2019,38(23):126-129.

    二、由一个广为流传的小例子了解Q学习的算法逻辑

    一个由门连接的建筑物中有五个房间,如下图所示,分别用0-4号标识,将外界看作一个大房间,同样标识为5。

    每个房间代表一个节点,门代表连线,可以将上图抽象为下面这样:

    agent会被随机放置在任意一个房间里,然后从那个房间出发,一直走到建筑外(即5号房间为目标房间)。门是双向的,所以相邻节点间是双向箭头连接。通过门可以立即得到奖励值100,通过其他门奖励值为0。将所有箭头标注奖励值如下图:

    在Q学习中,目标是达到奖励最高的状态,所以如果agent到达目标,它将永远在那里。 这种类型的目标被称为“吸收目标”。

    想象一下,我们的agent是一个想象的虚拟机器人,可以通过经验学习。 agent可以从一个房间转移到另一个房间,但它没有上帝视角,也不知道哪一扇门通向外面。

    按照第一部分Q学习的理论,我们把每个房间抽象为一个状态,选择进入哪号房间作为动作,把状态图和即时奖励值放到下面的奖励值表“回报矩阵R"中:(-1表示不可选择的动作,两个状态间没有连接)

    现在我们将添加一个类似的矩阵“Q”给我们agent的大脑,代表了通过经验学到的东西的记忆。 矩阵Q的行表示agent的当前状态,列表示导致下一个状态的可能动作(节点之间的连线)。

    agent开始什么都不知道,矩阵Q被初始化为零。 在这个例子中,为了解释简单,我们假设状态的数目是已知的(六个,0-5)。 如果我们不知道涉及多少个状态,矩阵Q可能从只有一个元素开始。 如果找到新的状态,则在矩阵Q中添加更多的列和行是一项简单的任务。

    Q学习的更新规则如下:

    根据这个公式,分配给矩阵Q的特定元素的值等于矩阵R中相应值加上学习参数γ乘以下一状态下所有可能动作的Q的最大值。

    每一场景的探索都会为agent增加经验,Q矩阵得到更新。训练的基本思路如下图:

    基于算法思想,训练Q矩阵的具体流程如下:

    步骤1.初始化仓库环境和算法参数(最大训练周期数,每一场景即为一个周期,折扣因子γ,即时回报函数R和评估矩阵Q。)。

    步骤2.随机选择一个初始状态s,若s=s*,则结束此场景,重新选择初始状态。

    步骤3.在当前状态s的所有可能动作中随机选择一个动作a,选择每一动作的概率相等。

    步骤4.当前状态s选取动作a后到达状态s’

    步骤5.使用公式对Q矩阵进行更新。

    步骤6.设置下一状态为当前状态,s=s‘。s未达到目标状态,则转步骤3。

    步骤7.如果算法未达到最大训练周期数,转步骤2进入下一场景。否则结束训练,此时得到训练完毕的收敛Q矩阵。

    上面的流程是给agent用来学习经验的。 每一场景都相当于一个培训课程。 在每个培训课程中, agent将探索环境 (由矩阵R表示), 接收奖励 (如果有), 直到达到目标状态。训练的目的是提高我们的agent的 "大脑"(矩阵 Q)。 场景越多,Q矩阵越优化。 在这种情况下, 如果矩阵 Q 得到了增强,四处探索时并不会在同一个房间进进出出, agent将找到最快的路线到达目标状态。

    参数γ的范围为0到1(0 <= γ< 1)。 如果γ接近零,agent将倾向于只考虑立即得到奖励值。 如果γ更接近1,那么agent将会考虑更多的权重,愿意延迟得到奖励。

    我们使用Python为训练agent编写代码:

    import numpy as np
    import random
    
    # 初始化矩阵
    Q = np.zeros((6, 6))
    Q = np.matrix(Q)
    
    # 回报矩阵R
    R = np.matrix([[-1,-1,-1,-1,0,-1],[-1,-1,-1,0,-1,100],[-1,-1,-1,0,-1,-1],[-1,0,0,-1,0,-1],[0,-1,-1,0,-1,100],[-1,0,-1,-1,0,100]])
    
    # 设立学习参数
    γ = 0.8
    
    # 训练
    for i in range(2000):
        # 对每一个训练,随机选择一种状态
        state = random.randint(0, 5)
        while True:
            # 选择当前状态下的所有可能动作
            r_pos_action = []
            for action in range(6):
                if R[state, action] >= 0:
                    r_pos_action.append(action)
            next_state = r_pos_action[random.randint(0, len(r_pos_action) - 1)]
            Q[state, next_state] = R[state, next_state] + γ *(Q[next_state]).max()  #更新
            state = next_state
            # 状态4位最优库存状态
            if state==5:
                break
    print(Q)
    

    运行结果为:

    这个矩阵Q可以通过将所有的非零条目除以最高的数字(在这种情况下为500)来归一化(即转换为百分比):

    一旦矩阵Q足够接近收敛状态,我们知道我们的agent已经学习了任意状态到达目标状态的最佳路径。

    例如:

    从初始状态2开始,agent可以使用矩阵Q作为指导,从状态2开始,最大Q值表示选择进入状态3的动作。

    从状态3开始,最大Q值表示有两种并列最优选择:进入状态1或4。假设我们任意选择去1。

    从状态1开始,最大Q值表示选择进入状态5的动作。

    因此,最优策略是2 - 3 - 1 - 5。

    同时,若状态3时选择进入状态4,最优策略为2-3-4-5。

    两种策略的累计回报值相等,故从状态2到5有两种最优策略。

    # 例子较为简单,读者可以根据流程图和步骤解释手算以便加深印象。

    展开全文
  • Marvell车载以太网交换机芯片88Q5050

    千次阅读 2022-01-29 17:08:58
    Marvell推出了四款车载用交换机芯片,88Q5050,88Q5050, 88Q5072和88Q6113。其中88Q5030有5 Port用于通信,88Q5050有8 Port用于通信,5072与6113有11 Port用于通信。由于项目中用到了88Q5050,所以本文中只涉及到88Q...
  • C++中是没有像其他后辈(C# Java)那样,提供枚举变量到字符串之间的快捷转换功能的,程序要打印枚举变量时,只能额外再写一个转换函数,费时费力,好在,Qt提供了这个功能,通过Q_ENUM宏和Q_FLAG宏,实现了更多更酷...
  • Q-Q图原理详解及Python实现

    千次阅读 2021-01-23 16:16:17
    【导读】在之前的《数据挖掘概念与技术 第2章》的文章中我们介绍了Q-Q图的概念,并且通过调用现成的python函数, 画出了Q-Q图, 验证了Q-Q图的两个主要作用,1. 检验一列数据是否符合正态分布 2. 检验两列数据是否...
  • Q-learning是一种典型的基于价值(Value)函数的强化学习方法,其中的Q是一个数值,通常在初始化时有可能被赋予一个任意数值(因问题场景而异),在迭代时刻ttt,我们有状态sts_tst​,此时代理做出动作ata_tat​,...
  • Q-learning原理及其实现方法

    千次阅读 热门讨论 2019-11-24 20:48:54
    Q_learning原理及其实现方法声明简介Q_learning算法Q_learning算法流程 声明 学习博客快乐的强化学习1——Q_Learning及其实现方法,加之自己的理解写成,同时欢迎大家访问原博客 简介 Q-Learning是一种 value-based ...
  • updated_q = q_old + (self.alpha * (reward+ self.gamma*self.nashq[state]- q_old)) def compute_nashq(self, state): nashq = 0 #遍历nash表 for action1 in self.actions: for action2 in self.actions: ...
  • 深度强化学习系列(5): Double Q-Learning原理详解

    万次阅读 多人点赞 2019-12-05 21:53:34
    论文地址: ...前言: Q-Learning算法由于受到大规模的动作值过估计(overestimation)而出现不稳定和效果不佳等现象的存在,而导致overestimation的主要原因来自于最大化值函...
  • 强化学习Q-learning(超详解)

    万次阅读 多人点赞 2020-07-08 22:05:11
    在学习强化学习的过程中,Q-learning是我们必须掌握的基础算法,那么什么是Q-learning,它的原理又是什么呢?
  • >李联不怕吃苦 3 成功学例子 加强巩固 除非你努力了,否则不能成功 除非p,否则不q 结论:q->p ~p-> ~q p:努力了 q:能成功 q->p: 能成功说明你努力了 ~p-> ~q:不努力肯定不成功 错误推理:努力了就成功了p->q 除非...
  • IQ数据简介:I/Q Data

    千次阅读 多人点赞 2020-10-12 17:00:16
    I:in-phase 表示同相 Q:quadrature 表示正交,与I相位差90度。 IQ调制就是数据分为两路,分别进行载波调制,两路载波相互正交。I是in-phase(同相), q是 quadrature(正交)。
  • 【强化学习】Q-Learning算法详解

    万次阅读 多人点赞 2018-06-19 21:18:18
    QLearning是强化学习算法中值迭代的算法,Q即为Q(s,a)就是在某一时刻的 s 状态下(s∈S),采取 a (a∈A)动作能够获得收益的期望,环境会根据agent的动作反馈相应的回报reward r,所以算法的主要思想就是将State与...
  • STM32CbueMX之W25Q256

    千次阅读 2019-06-21 08:49:15
    W25Q128和W25Q256指令通用,只是一个是24bit地址另一个是32bit地址。 驱动应该通用(没测试过)。不行就去除BSP_W25Q256_Enter4ByteAddressMode()函数,然后删除read wirte函数的A31-A24位。 /********...
  • Android Q 适配,看这篇就妥了

    千次阅读 2019-06-04 09:00:00
    Android Q Beta 1刚出,讲道理国内是不到下半年不用理睬Q的,但是上月末的一封华为要求适配Q的邮件要求我们在5月底之前完成相关适配,不然应用会被下架。 一开始还心生奇怪,为什么这次华为的邮件来的那么早以及严格...
  • 第一部分:W25Q128代码头文件 (W25Q128.h) #ifndef W25Q128_H #define W25Q128_H #include "stm32f10x.h" #include "stdio.h" #include "sys.h" #include "delay.h" #define W25Q128_CS PBout(12) #define W25Q...
  • 实现的内容很简单,存为.m文件可以直接在matlab上运行,就是利用Q学习(Q learning)完成自主路径寻优简单示例,并进行可视化,Q学习部分参考了如上链接中的内容,供大家交流学习使用,请多提宝贵意见 如图为最终...
  • 强化学习(Q-Learning,Sarsa)

    万次阅读 多人点赞 2019-03-25 18:34:16
    Q(S,A)=Q(S,A)+α(R+γQ(S′,A′)−Q(S,A))Q(S,A) = Q(S,A) + \alpha(R+\gamma Q(S',A') - Q(S,A))Q(S,A)=Q(S,A)+α(R+γQ(S′,A′)−Q(S,A)) 算法流程为: 同样建立一个Q Table来保存状态s和将会采取的所有动作a,...
  • Q学习和深度Q学习(DQN)论文笔记

    万次阅读 多人点赞 2019-01-02 15:20:46
    Q学习(Q-learning) ...Qπ(st,at)=E[r+γE[Qπ(st+1,at+1)]]Q^\pi(s_t,a_t)=E[r+\gamma E[Q^\pi(s_{t+1},a_{t+1})]]Qπ(st​,at​)=E[r+γE[Qπ(st+1​,at+1​)]] 这个公式实际上也揭露了状态的马尔科夫性质,也...
  • 强化学习——Q学习算法

    万次阅读 2020-06-07 23:42:24
    qMax(x1)代表x1这个状态下的最好奖励 % 判定是否收敛 其实就是前后两个Q矩阵的差很小就认为是收敛了 if sum(sum(abs(Q1-Q)))(sum(Q>0)) %sum(sum(Q>0))代表Q大于0的数的个数,sum(sum(abs(Q1-Q)))代表Q1与Q对应位置...
  • 时间序列——MA(q)模型

    万次阅读 2019-04-26 21:26:57
    在讨论时间序列的MA(q)模型前,我们首先了解以下相关的概念 1、协方差:反映的是随机变量之间的关系,类似协方差函数,在时间序列里,我们可以给出自协方差的概念。因为时间序列是一维的,没法找到一个别的数据和...
  • W25Q64是华邦公司推出的大容量SPI FLASH产品,其容量为64Mb(8MB),应用较为广泛。 W25Q系列的器件在灵活性和性能方面远远超过普通的串行闪存器件。W25Q64将8M字节的容量分为128个块,每个块大小为64K字节,每个块...
  • transformer中QKV的通俗理解(渣男与备胎的故事) Attention is all you need
  • Android Q的全新特性与隐私权限

    万次阅读 2019-05-10 23:50:53
    在前几天的Google I/O 2019大会上,发布了Android Q版本(Android 10)。Android Q带来了许多新特性,也增强安全隐私保护,包括支持折叠屏、非SDK接口限制、共享内存、分区存储、系统二进制文件映射到只执行内存、WLAN...
  • Q绑查询HTML源码

    千次阅读 2021-08-12 11:05:11
    简介: 可用的查询源码,支持本地查询或者上传到空间访问,支持上传二级目录访问! 网盘下载地址: http://kekewl.cc/6C9hSq3ZsHi0 图片:
  • Android Q(10.0)版本新特性和兼容性适配

    万次阅读 热门讨论 2019-04-18 12:02:29
    北京时间2019年3月14日Google正式对外发布Android Q Beta 1及预览版SDK,这意味着安卓开发者们又即将迎来一年一度的新版本适配工作了。Android Q 为开发者们带来了许多新功能,如折叠屏增强项、新网络连接 API、全新...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,735,932
精华内容 1,094,372
关键字:

q