结合手册调试各个功能的寄存器+最后通过寄存器编写SPI的初始化和读写函数

前言:在前面的学习中,我已经初步了解每个功能所涉及的寄存器以及调用关系,接下来我将结合手册去对这八个功能的例程函数内部的寄存器一个个去研究,我相信这将让我对底层有更深入的了解,并且很快通过这个测试。

GPIO:

我们知道GPIO的使用共三步:1、使能想要的GPIO口时钟,2、初始化结构体,3、将初始化的结构体传入想要引用GPIO口,

如上面两张图,我们进入使能时钟函数内部后接着运行我们会看到RCC_AHB1Periph的值为0x00000020,接着我们跳到RCC_AHB1Periph_GPIOF的定义,如第一张图就是0x00000020,这4字节是什么呢?我们可以点开AHB1ENR,查看它的定义:

c 复制代码
__IO uint32_t AHB1ENR;       /*!< RCC AHB1 peripheral clock register,                          Address offset: 0x30 */

可以看到它的偏移地址是0x30,那么我们就可以翻开手册去RCC的偏移地址0x30看看,如下

可以看到0x00000020就是对bit5(GPIOF)置1,所以这个使能时钟函数就是对GPIOF的时钟使能

接着我们跳到结构体类型函数:

可以看到函数执行完,其中有4个成员的参数被赋值了,我们一个个点进去看看

可以看到Mode、Speed、OType、Pin分别被赋值:0x01、0x02、0x00、0x000001C0,那么我们翻开手册看看

可以看到这些都是对不同寄存器的操作,都是可以对应上的,至于GPIO_PuPd因为我们没有传参,所以没有被定义,所以保存残留野值,没有实际意义,为什么没有意义,虽然这个值也会被传入PuPd,但是我们配置的是输出模式所以上下拉对我们没有影响

接着我们跳进GPIO_Init()函数看看:

代码逻辑我们之前了解过了,就是利用for循环,每轮循环检测 GPIO_Pin 的第 pinpos 位是不是 1,是的话就进入if()语句,然后再利用currentpin判断是否是我们传进来的引脚,是的话就进入if()语句里面(在第二张图中,可以看到当pos:0x00000040时,就成功进入if语句了,对应的就是输出寄存器的bit6------pin_6)对GPIOx成员的各种寄存器进行赋值

GPIOx就是第一个参数,我们选择的是GPIOF,即取GPIOF的基地址 ,因为有些寄存器是2个bit控制的,比如Mode寄存器:

所以就可以先通过pinpos*2来先将准备要控制的寄存器清0(主要是::MODER 一个寄存器管 16 个引脚、每引脚 2 位------你不能整个寄存器清零,否则其他 15 个引脚的配置全毁,次要:防止之前定义过而留下来的没有处理的数据对本次操作进行干扰)

最后我们可以看GPIO_SetBits()函数

可以看到执行完GPIO_SetBits(GPIOF,GPIO_Pin_6)函数,ODR寄存器的值变成0x00000040,对应的就是输出寄存器的bit6,这就是通过给BSSRRL赋值做到的,我们到BSSRRL定义看看

偏移地址0x18,手册如下:

可以看到低16位即BSy,置1就会对相应的ODRx位进行置位,而BSRR 高 16 位是 BRy:写 1 是清零。所以 GPIO_SetBits 用低 16 位(BSRRL),GPIO_ResetBits 用高 16 位(BSRRH)。结合ODR寄存器来看

可以看到BSRR就是对ODR寄存器起辅助功能,可以通过BSRR操作ODR的对应的寄存器

为什么需要BSRR只写寄存器?

因为直接写 ODR:编译器要"读 ODR → 改 → 写回"三步,中途被中断打断,就可能把其他引脚误;BSRR 是只写寄存器:写 1 置位、写 0 无效 → 置位某引脚时天然不碰其他位,无需读-改-写,天然原子。

RCC:

时钟树框图:

通过这个框图我们能很好的梳理接下来的设置系统时钟函数内部控制寄存器的顺序

现在我们进入User_SetSysClock()函数内部看看:

首先我将过程涉及到的寄存器都放进Watch窗口,可以看到这些都是复位值,随后我们一步步执行看看这些寄存器值发生了什么变化

可以看到执行完RCC_DeInit()函数和RCC->CR |= ((uint32_t)RCC_CR_HSEON);,有三个寄存器值发生了变化我们根据手册去看看

首先可以看到CFGR和PLLCFGR寄存器都被配置为复位值,也就是说RCC_DeInit()函数是初始化这些寄存器的配置

而CR寄存器的bit7、10、15被置1,我们去看看这些bit位控制什么

可以看到这应该就是配置内部时钟校准,随后我们继续单步运行:

可以看到bit16、17被置1了,根据手册就是使能HSE和HSE就绪标志位,我们可以去看看改变CR寄存器的变量定义:

配合这两段定义不难理解:首先通过RCC_CR_HSEON 给CR寄存器的HSE时钟使能,接着就是通过循环不断检测这个就绪标志位,但这个就绪位置1或者轮询次数等于这个HSE_STARTUP_TIMEOUT变量就跳出循环

随后可以看到通过这个变量来HSEStatus进入if()语句里面,这个变量但if ((RCC->CR & RCC_CR_HSERDY) != RESET)条件为真HSEStatus被赋值0x01,即HSE就绪位置1就进入if()语句

进入if()语句我们可以看到 RCC->APB1ENR,PWR->CR 的值并没有改变,为什么呢?我们可以先到手册去看这个值对应什么功能先,后面我会给答案:

可以看到:RCC->APB1ENR |= RCC_APB1ENR_PWREN对应使能电源接口时钟,PWR->CR |= PWR_CR_VOS对应调压器输出电压级别选择模式1,回到开头为什么这个值没有发生变化,或者说为什么这两个寄存器一开始就已经被赋值了,因为在system_stm32f4xx.c 里 SystemInit()里这些寄存器都被配置过了,我它去改启动文件的汇编 禁用SystemInit,所以APB1ENR和CR寄存器已经被赋值过了

当执行到这一步的时候CFGRbit11(即100)被置1了,通过手册可以看到APB2总线的时钟是系统时钟的2分频

执行到这一步CFGR寄存器又被赋值了,虽然这一步是刚刚执行RCC->PLLCFGR ,但CPU好像还没运行完,还在执行前面的RCC->CFGR赋值,到这里才真正给CFGR寄存器配置完,对应手册就是APB1总线的时钟是系统时钟的4分频

上面配置APB1和APB2总线时钟对应的就是时钟树的第5步

这里PLLCFGR寄存器的值被改变的,对应的是77行代码:

时钟树对应的就是配置锁相环倍频

就是公式如上,PLL_M、PLL_N 、PLL_P、PLL_Q 都是配置对应的分频、倍频因子,不多介绍,而RCC_PLLCFGR_PLLSRC_HSE配置的是时钟树的第2步:PLL时钟源选择

RCC_PLLCFGR_PLLSRC_HSE的定义如下:

对应手册就是PLLSRC被置1,所以这里选择HSE外部高速时钟为PLL时钟源

接着又是配置CR寄存器,可以看到bit24被置1了,对应的就是开启PLL(锁相环) ,所以可以总结:先配置PLL,在使能PLL

可以看到FLASH->ACR寄存器还是没有改变,道理跟前面的配置电源控制寄存器是一样的

但是我们可以看到CFGR寄存器的值又改变了

从这个定义和代码逻辑来看,寄存器应该被配置成这样

没错,这里对应时钟树第四步:选择PLL作为系统时钟,这就完成了对系统时钟的初始化配置

USART:

USART框图:

前面GPIO引脚配置我在前面已经说过,就不细说了,我们着重进入GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1)这个函数来看看内部怎么实现寄存器配置,在内内部实现前,我们先去看手册,大概了解一下要配置的寄存器:



让我来解释一下这三幅图,前面的复用寄存器分为了
低位寄存器和高位寄存器:即4个字节控制8个引脚,4个bit对应一个引脚
,而且下面的AF则是对应的复位功能,那我怎么知道哪个AF对应什么复位功能呢?这个在数据手册有,即第三个图片,因为我们配置的GPIOA,所以我截屏的是GPIOA的AF映射表

接下来我们到该函数内部看看寄存器值的变化:

可以看到执行到这一步,temp被赋值0x00000700,这是什么意思呢?

我们先对这段((uint32_t)(GPIO_AF) << ((uint32_t)((uint32_t)GPIO_PinSource & (uint32_t)0x07) * 4)) 代码进行分析:

(uint32_t)((uint32_t)GPIO_PinSource & (uint32_t)0x07)等价于 0000 1010 & 0000 0111=0000 0010,0000 0010=2,我们看图片二,引脚10不就对应24bit的位置吗?**所以在复用功能高位寄存器(<<24)就是对应的引脚10,而GPIO_AF<<2*4就是将这个AF对应的复用功能赋值给我们配置的引脚10**,所以这段代码就是将我们想要的配置的AF值移到对应的4bit

接着对这段GPIOx->AFRGPIO_PinSource \>\> 0x03 &= ~((uint32_t)0xF << ((uint32_t)((uint32_t)GPIO_PinSource & (uint32_t)0x07) * 4)) 代码分析:

首先AFRGPIO_PinSource \>\> 0x03 这个相当于0000 1010>>3=1,还记得我们前面说过的4个字节控制8个引脚,4个bit对应一个引脚吗?所以AFR\[\]是两个寄存器,即AFR1对应的就是复用功能高位寄存器 ,基于前面的,我们知道0xF << (GPIO_PinSource & 0x07)* 4等价于1111<<2*4,前面还有一段运算(&= ~),所以这段代码就是先计算出你传进来的引脚对应的寄存器,接着对该引脚4bit清零

最后就是 temp_2 = GPIOx->AFRGPIO_PinSource \>\> 0x03 | temp;GPIOx->AFRGPIO_PinSource \>\> 0x03 = temp_2;这两段代码,就是将AF值赋值给传进来的引脚,这两段代码等于GPIOx->AFRGPIO_PinSource \>\> 0x03 | temp;那为什么还要temp_2这个变量呢?

接着就是对APB2外设时钟bit4置1,然后是初始化USART结构体

赋值完最后一个,我们跳到USART_Init()函数内部看看

首先是控制CR2寄存器的停止位,在前面给结构体赋值的时候我们可以看到,我们选择的是1个停止位即对bit12、bit13赋值00,所以右边tmpreg和USARTx->CR2都等于0,不过还是可以分析代码逻辑:首先将CR2寄存器的值赋给tmpreg (tmpreg = USARTx->CR2),然后单独给停止位清零 (tmpreg &= (uint32_t)~((uint32_t)USART_CR2_STOP)),接着将结构体的值赋给tmpreg这个变量 (tmpreg |= (uint32_t)USART_InitStruct->USART_StopBits;),最后将这个tmpreg变量的值赋给CR2寄存器(USARTx->CR2 = (uint16_t)tmpreg;)

接着是对CR1寄存器赋值:

这里的代码逻辑跟上面是一样的,就不多描述了,主要是看手册对CR2寄存器的赋值

USART_InitStruct->USART_HardwareFlowControl是对这两个bit的控制,对应上面框图:

这个我们用不到,所以对这两位bit置0

最后就是对波特率的计算,这里RCC_GetClocksFreq(&RCC_ClocksStatus)是取这个总线的系统时钟频率

可以看到执行完USART_Init()函数后,USART_InitStruct结构体的成员又发生了变化,我猜测这些都是随机赋值

接着是USART_Cmd(USART1,ENABLE)跳到该函数内部

接着我们进入USART1_SendByte(USART1,'A')函数看看

可以看到,一进入该函数,SR寄存器就被置位了

注意看手册的复位值,所以这不是代表数据发送完成了。而且这个寄存器复位值就是如此

接着我们进入while(USART_GetFlagStatus(USARTx,USART_FLAG_TXE)==RESET);这个函数

可以看到一进到该函数,TXE就置1了,该代码逻辑就是判断USARTx->SR寄存器的TEX位是否置1,置1就返回SET,否则返回RESET

接着我们到USART_SendData(USARTx,ch )函数内部看看:

可以看到执行完最后一步,USARTx->DR 寄存器值都没有发生变化,当看串口接收器,是能看到单片机发送过来的数据的,所以我猜测是我们刚刚将数据填入到数据寄存器中,硬件就马上将数据发送给电脑了,所以我们来不及观察它的变化

我在之前提到过可以用中断的方式来接收数据,具体流程就是:当接收数据寄存器收到电脑发来的数据,对应的状态寄存器就会置1,随后就会向NVIC发出中断请求(即产生中断),这个请求是需要到对应的控制寄存器置1的,本节将使能接收寄存器非空中断,手册详细如下:

接着我们进入USART_ITConfig(USART1,USART_IT_RXNE,ENABLE)该函数内部看看如何实现寄存器的控制:

可以看到一进到该函数usartxbase的值就发生了变化,而这个就是我们传进来的USART1的基地址

随后是usartreg变量也被赋值了,这个是用来判断是哪个控制寄存器 ,在手册中我们可以看到有3个控制寄存器,所以这个变量是来判断,我们使能了哪个控制寄存器的中断,那这段代码逻辑是怎么实现的呢?往下看:

我们可以看到我们传进来的使能哪个中断定义是0x0525(即0x0000 1010 0010 0101) ,注意这段代码:

usartreg = (((uint8_t)USART_IT) >> 0x05),是指USART_IT舍弃高八位,并且右移五位(即001) ,所以这三位bit是用来判断我们用到的寄存器,很显然我们使能的RXNE中断是控制寄存器1

接着itpos变量也发生了变化,值是USART_IT & IT_MASK,所以我们先看看IT_MASK的宏定义

可以看到IT_MASK为0x0000 0000 0001 1111,所以这段代码就是0x0000 1010 0010 0101 & 0x0000 0000 0001 1111,可以得到0x0005,而我们看手册RXNEIE的位置就是CR1的bit5,所以这段代码就是判断我们使能的中断是哪个位,低4位就是用来计算这16bit的位置

itmask 这个变量就是保存我们使能哪个bit位,随后根据我们前面得到的usartreg,就可以得到这个寄存器的偏移量,最后只要我们传进来ENABLE,(__IO uint32_t)usartxbase |= itmask;

既然使能了NVIC中断申请,当然也要配置NVIC,初始化NVIC包括:NVIC分组、配置NVIC结构体、初始化NVIC,但是流程观察不了,因为我没在手册找到关于NVIC的寄存器数据

总结:

1.GPIO口总线使能:

使能总线:AHB1-RCC

地址 :0x4002 3800

寄存器:

RCC AHB1 外设时钟使能寄存器(偏移地址:0x30;用到的位:bit0;位作用:使能GPIOA)

2.初始化GPIOA和选择复用功能

地址 :0x4002 0000

寄存器:

GPIO端口模式寄存器(偏移地址:0x00;用到的位:bit18~21;位作用:选择端口模式)

GPIO端口输出类型寄存器(偏移地址:0x04;用到的位:bit9;位作用:选择输出类型)

GPIO端口输出速度寄存器(偏移地址:0x08;用到的位:bit18、19;位作用:选择输出速度)

GPIO端口上\下拉寄存器(偏移地址:0x0C;用到的位:bit20、21;位作用:选择输入端口上下拉)

GPIO复用功能高位寄存器(偏移地址:0x24;用到的位:bit4~11;位作用:配置端口复用功能)

3.USART总线使能:

使能总线:APB2-USART

地址 :0x4002 3800

寄存器:

RCC APB2 外设时钟使能寄存器(偏移地址:0x44;用到的位:bit4;位作用:使能USART)

4.初始化USART、使能USART、中断:

地址 :0x4001 1000

寄存器:

状态寄存器(偏移地址:0x00;用到的位:bit5、6、7;位作用:bit5RXNE、bit6TC、bit7TXE)

数据寄存器(偏移地址:0x04)

波特率寄存器(偏移地址:0x08)

控制寄存器CR1(偏移地址:0x0C;用到的位:bit(2、3)、5、10、12、13;位作用:bit2、3使能发送器和接收器,bit5使能接收数据寄存 器非空中断,bit10是否使能奇偶校验,bit12选择字长,bit13使能USART)

控制寄存器CR2(偏移地址:0x10;用到的位:bit(12、13);位作用:选择停止位大小)

控制寄存器CR3(偏移地址:0x14;用到的位:bit(8、9);位作用:bit(8、9)是否使能硬件流控制)

I2C:

首先是配置I2C_InitStruct结构体,然后进入 I2C_Cmd(I2C1,ENABLE)函数里面:

这一段代码是配置I2C时钟基准频率,与前面配置的结构体参数无关,这里是根据I2C搭载在APB1总线的时钟频率来配置I2C_CR2的FREQ位(I2C控制寄存器2 )如下:

tmpreg = I2Cx->CR2;tmpreg &= (uint16_t)~((uint16_t)I2C_CR2_FREQ);这两段代码看多了就知道,是把FREQ位清0 ,RCC_GetClocksFreq(&rcc_clocks);pclk1 = rcc_clocks.PCLK1_Frequency;这两段代码是取APB1总线的频率赋值给pclk1,

freqrange = (uint16_t)(pclk1 / 1000000);tmpreg |= freqrange;因为FREQ是以MHz为单位的,对于APB1总线42MHz,自然要 / 1000000(1M),最后就按这个时钟频率给I2C_CR2置位(从窗口看freqrange =0x002A(16*2+10=42)就可以知道)

接着来配置I2C_CCR和I2C_TRISE、I2C_CR1

首先是将CR1中PE位置0,因为2C_TRISE寄存器要在CR1-PE置0下才能置位,这个寄存器与前面的配置I2C基准时钟一样与结构体参数无关,不过还是要初始化配置

if (I2C_InitStruct->I2C_DutyCycle == I2C_DutyCycle_2)这个是判断我们选择的是哪种占空比,根据我们的占空比来计算CCR

可以看到经过计算result值变化为0x0023(即35)

可以看到tmpreg值变化为了0x8023,看手册可以知道这是将F/S置位1,也就是选择快速模式 ,因为我们在结构体中传入的参数是400000,在else /(I2C_InitStruct->I2C_ClockSpeed <= 400000) /这段代码中已经判断出我们选择的是快速模式了

接着是经过(uint16_t)(((freqrange * (uint16_t)300) / (uint16_t)1000) + (uint16_t)1); 计算,按计算结果(0x000D)给TRISE赋值,

同时最后按tmpreg值给CRR寄存器置位,同时重新给I2C_CR1_PE置1(外设使能)

最后就是置位I2C_CR1和I2C_OAR1(自有地址寄存器),因为我们使用的主模式,所以这个2C_OAR1寄存器应该保持默认值0

tmpreg = I2Cx->CR1;tmpreg &= CR1_CLEAR_MASK;这两步是给CR1寄存器的ACK、SMBTYPE和SMBUS bits清0.这3bit对应的功能如下:

按结构体参数配置分别置位1、0、0(SMBTYPE置啥都没关系,因为我们选择的是I2C)

我们可以看到I2Cx->CR1和I2Cx->OAR1的值好像跟我们想象的不一样,I2Cx->CR1的值不应该是0x0401吗?还有I2Cx->OAR1的值怎么是给bit14置1啊,手册这个位没有任何功能啊?

因为这都是程序的bug:

(1)ENABLE 是 FunctionalState 枚举,值 = 1 = 0x01。而 I2C_Ack 这个成员要求的是 I2C_Ack_Enable(0x0400)/ I2C_Ack_Disabl(0x0000)。0x01 恰好落在 bit0 上------那本来就是 PE 位,bit10 根本没被置上。(所以如果程序出现错误,可以去把这个ENABLE值设置回0x0400或者在初始化手动调用I2C_AcknowledgeConfig(I2C1,ENABLE);)

(2)宏 I2C_AcknowledgedAddress_7bit = 0x4000 是 F1 时代库的遗产。F1 手册(RM0008)要求 OAR1 bit14"必须保持 1",所以 F1 库把这个宏定义成 0x4000。F4 库从 F1 库抄代码时原样带了过来,但 F4 手册里 bit14 已经没这个要求了(F4 的 10 位地址改用 bit9:0,bit14 变保留位)。

接着来到I2C_Cmd(I2C1,ENABLE)函数内部

可以看到这个函数还是给CR1寄存器PE位置1,我们观察到不是给PE位置1了还,怎么这里还要使能外设啊?我猜测是保险起见再使能一次

下面我们就进入读写函数内部

我们进入到该I2C_GenerateSTART(I2C1,ENABLE)

可以看到执行到最后一步SR1和SR2的值发生了变化,回到手册找到对应的位

可以看到SR1bit0置1代表起始位已发送,SR2bit0和bit2置1代表,我们主机选择主模式向从机发送"读"指令

但是我们也可以看到,代码是访问CR1啊,为什么窗口的CR1值没有发送变化呢?我们去手册看看

从手册的信息可以知道,这是给bit8置1,注意手册中写"可在起始位发送完成后由硬件清零",我们在上面观察到SR1的bit0位已经置1了,所以我CR1bit8成功置1了,只是硬件擦除太快了,我们观察不到

接下来我们访问这个I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)

这三步是访问SR1和SR2,访问SR1是为了获取各种状态标志位,访问SR2是为了获取MSL位(选择主模式/从模式),随后flag2 = flag2 << 16是位图对齐:把整个 SR2 抬到高 16 位,和事件宏的定义对上(EV5 = MASTER_MODE_SELECT│ 0x00030001 │ SB + (MSL|BUSY)<<16 )

,我们可以看到窗口flag1是bit0置1,在手册中就是SB位(起始位) ,而我们看手册

可以知道EV5就是检验SB位的,接着lastevent = (flag1 | flag2) & FLAG_MASK这段代码就是将SR1和SR2的值赋给lastevent,然后通过lastevent的值与我们传进来的参数(事件参数)对比是否一致(我们的状态寄存器置位与一致说明该事件的宏定义一致,说明该事件发送了),然后返回成功

接着是I2C_Send7bitAddress(I2C1,0XA0,I2C_Direction_Transmitter);函数

上面那段if()else()语句就是根据我们参数三来给传进来的Address最后一位置位(所以我们在传地址的时候,最后一位可以随便填,调用这个函数的时候在根据反向填参数三 ),执行到最后一步我们可以看到三个寄存器值发生了变化,DR不用说就是将地址+读写位 传入,其它两个置位作用呢?

按手册找SR1置位对应这两个事件,数据寄存器为空代表了地址已经发送出去了,而且ADDR位置1代表着事件6发生了,看手册可以知道读取SR2后这个位会被硬件清零,所以等我们调用前面的I2C_CheckEvent()就会分别读取SR1和SR2

SR2置位对应作用

这是检测事件函数内部,可以看到SR1中bit2在读取SR2后已经被清零了

接着是I2C_SendData(I2C1,address);

这个函数内部操控的寄存器很简单,就是将传进来的数据写入DR寄存器(这里传进来的主机要写入从机的数据起始地址)

接着是I2C_GenerateSTOP(I2C1,ENABLE);

该函数操控的是CR1,跟据我们的参数三来写入,可以看到当我们给停止位置1后,SR1和SR2的其它位都被清零了,这与我给你们看手册描述一致

最后是I2C_ReceiveData (I2C1)函数:

这是SR2对应的位,可以看到接收bit2置0

最后看DR数据寄存器也成功传入数据

SPI:

前面的配置GPIO和引脚复用就不多描述了

首先是SPI的初始化配置,我们先来看结构体参数:

接着来到 SPI_Init(SPI1,&SPI_InitStruct)内部:

这两步是只保留了CR1寄存器的bit12、13,bit5(初始化BR),其它清零

这一步就是将传进来的结构体参数赋值给tmpreg,然后按tmpreg值给CR1寄存器置位,可以看到窗口CR1置位

对应的手册如下:

接着是对其它两个寄存器置位

SPIx->I2SCFGR &= (uint16_t)~((uint16_t)SPI_I2SCFGR_I2SMOD);这一步是将SPI_I2S配置寄存器的bit11位清0,即默认使用SPI模式

SPIx->CRCPR = SPI_InitStruct->SPI_CRCPolynomial;这一步是按照结构体参数置位CRCPR

接下来是使能SPI外设函数

可以看到CR1对比原来的值,bit6被置1了,看手册对应的功能就是SPI使能

接着我们来到SPI_Byte_Write(uint8_t Data)函数

SPI_GetFlagStatus(SPI1,SPI_FLAG_TXE)==RESET,我们进到该函数内部

可以看到该函数内部就是读取SR寄存器对比我们传进来的FLAG参数,如果相等就返回SET,否则返回RESET

随后我们进入SPI_SendData(SPI1,Data);内部,可以看到执行完最后一步,SR和DR的值都改变了,对于SR,我们看手册

可以看到对比原来,bit0置1了(即接收缓冲区非空)说明收到Flash传来的数据了而看到DR寄存器的值变成0xFF,这个就是Flash传过来的数据,说明我们的数据也发送成功了

接着我们进入while(SPI_GetFlagStatus(SPI1,SPI_FLAG_RXNE)==RESET);

该内部跟上面读SR寄存器TXE位是一样的,不过这里要注意:因为我们窗口添加了DR寄存器,所以窗口会读一次DR,如果读DR,RXNE就会清零,这就会造成一直卡在这个循环里

接着就是 RX_Data=SPI_ReceiveData(SPI1);函数

我们可以看到SR中RXNE位又置1了,说明我们接收到了数据,执行完后,可以看到RX_Data确实接收到了数据,还没各种读写函数都是依靠这个函数来实现的,所以不用单步执行了,下面我开始用寄存器SPI的初始化和读写函数

c 复制代码
/*初始化I2C1*/
void SPIConfig_Init(void)
{
    //RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);
    //RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG,ENABLE);
	*(uint32_t*)0x40023830 |=0x00000042;

    /*开启GPIO复用功能*/
//    GPIO_InitTypeDef GPIO_InitStruct;
//    GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF;
//    GPIO_InitStruct.GPIO_OType=GPIO_OType_PP;
//    GPIO_InitStruct.GPIO_PuPd=GPIO_PuPd_NOPULL;
//    GPIO_InitStruct.GPIO_Pin=GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5;
//    GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
//    GPIO_Init(GPIOB,&GPIO_InitStruct);
	 *(uint32_t*)0x40020400 |=0x00000A80;
     *(uint32_t*)0x40020404 |=0x00000000;
     *(uint32_t*)0x40020408 |=0x00000A80;
     *(uint32_t*)0x4002040C |=0x00000000;

    /*配置片选引脚,本节选择软件控制*/
//    GPIO_InitStruct.GPIO_Mode=GPIO_Mode_OUT;
//    GPIO_InitStruct.GPIO_Pin=GPIO_Pin_6;
//    GPIO_Init(GPIOG,&GPIO_InitStruct);
    *(uint32_t*)0x40021800 |=0x00001000;
    *(uint32_t*)0x40021804 |=0x00000000;
    *(uint32_t*)0x40021808 |=0x00002000;
    *(uint32_t*)0x4002180C |=0x00000000;

    /*选择哪个复用功能,这里选择SPI1*/
//    GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1);
//    GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1);
//    GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1);
	*(uint32_t*)0x40020420 |=0x00555000;


    // RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
    *(uint32_t*)0x40023844 |=0x00001000;

    // SPI_InitTypeDef     SPI_InitStruct;
    // SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
    // SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;
    // SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low;
    // SPI_InitStruct.SPI_CRCPolynomial =0;
    // SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
    // SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    // SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
    // SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
    // SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;
    // SPI_Init(SPI1,&SPI_InitStruct);
    /*配置CR1*/
   *(uint32_t*)0x40013000 |= 0x00000304;
   /*配置CRC*/
   *(uint32_t*)0x40013010 |= 0x00000000;
   /*配置SPI_I2S配置寄存器*/
   *(uint32_t*)0x4001301C |= 0x00000000;

     //SPI_Cmd(SPI1,ENABLE);
     *(uint32_t*)0x40013000 |= 0x00000344;
}

/*SPI发送和接收一个字节数据*/
uint8_t SPI_Byte_Write(uint8_t Data)
{
    uint8_t RX_Data;

    /*等待发送数据寄存器为空*/
    // while(SPI_GetFlagStatus(SPI1,SPI_FLAG_TXE)==RESET);
    // SPI_SendData(SPI1,Data);
    while((*((volatile uint32_t*)0x40013008) & 0x0002)==RESET);
    // SPI_SendData(SPI1,Data);
    (*((volatile uint32_t*)0x4001300C)) =(uint16_t)Data;


    /*等待接收数据寄存器非空*/
    // while(SPI_GetFlagStatus(SPI1,SPI_FLAG_RXNE)==RESET);
    // RX_Data=SPI_ReceiveData(SPI1);
     while((*((volatile uint32_t*)0x40013008) & 0x0001) == RESET);
    // RX_Data=SPI_ReceiveData(SPI1);
    RX_Data = (uint16_t)(*((volatile uint32_t*)0x4001300C));


    return RX_Data;
}

注意:在调用SR和DR寄存器的时候,要在前面加个volatile告诉编译器这个地址的值随时会被改变,要去内存读取,如果不加会怎么样呢?

(1)while((((volatile uint32_t )0x40013008) & 0x0002)==RESET)😭((volatile uint32_t )0x4001300C)) =(uint16_t)Data;在这两句中,如果不加,编译器只能读一次,所以第一次循环是能通过的,但是到了第二个循环while((((volatile uint32_t )0x40013008) & 0x0001) == RESET);因为编译器把第一次读到的值缓存在 CPU 寄存器里,之后每次循环只比较寄存器里的旧值,不再访问内存,而内存上这个地址的值已经被硬件改变了(即RXNE置1),所以就会造成虽然这个地址的值已经改变了,但程序还是会卡在第二个循环里。

(2)如果 (((volatile uint32_t )0x4001300C)) =(uint16_t)Data; RX_Data = (uint16_t)(((volatile uint32_t )0x4001300C));这两句不加volatile,在编译器视角就是:中间没有任何语句写这个地址(RXNE 循环读的是 0x40013008,不是 0x4001300C)→ "读到的必然是你刚写的值,那我直接把读替换成 Data 就行,那个写就是多余的" → 删除写指令。但是我们调用读函数的时候,主机是要发0xFF(即Data)这个值跟FLash内存值交换的,所以如果不加volatile,我们读到的就是我们要发送的0xFF,而不是Flash发送过来的值,还有一种可能因为这个写指令被删除了,所以DR寄存器永远不会被写入,那RXNE就永远不会置1,所以也会卡死在循环二

相关推荐
小程序设计4 小时前
本科毕设避坑|大数据 / 单片机物联网 / 网络工程 / PLC / 机械设计选题与落地心得
java·hive·hadoop·单片机
Zw-awa9 小时前
先让程序开口说话:为什么把人机交互放在最前面
stm32
Zwawa9 小时前
把按键和 LED 状态显示出来:第一次看见程序到底在干什么
单片机·嵌入式
Zw-awa9 小时前
把按键和 LED 状态显示出来:第一次看见程序到底在干什么
stm32
Zw-awa9 小时前
面包板接线为什么这样接:从模块需求到芯片资源分配
stm32
小程序设计9 小时前
基于STM32的办公室智能空气净化系统设计
stm32·单片机
Zw-awa10 小时前
用 OLED 给 STM32 做实时调试面板
stm32
Zw-awa10 小时前
按键为什么会乱跳:一次按下为什么变成很多次
stm32
不悔哥6 天前
开源OV-Watch:怎么做一个智能手表
单片机·开源·嵌入式