当前位置: 首页 > news >正文

网站该怎么找到宿州高端网站建设公司哪家好

网站该怎么找到,宿州高端网站建设公司哪家好,昆明做网站建设企业推荐,摄影网站的模板正点原子B站视频地址#xff1a;https://www.bilibili.com/video/BV1Lx411Z7Qa?p4spm_id_frompageDriver 目录STM32命名规则STM32芯片解读开发环境搭建(MDK - 就是ARM的keil,需破解 支持包 CH340串口驱动 JLINK驱动)程序下载方法 (ISP串口下载 JLINK下载更方便)新建工… 正点原子B站视频地址https://www.bilibili.com/video/BV1Lx411Z7Qa?p4spm_id_frompageDriver 目录STM32命名规则STM32芯片解读开发环境搭建(MDK - 就是ARM的keil,需破解 支持包 CH340串口驱动 JLINK驱动)程序下载方法 (ISP串口下载 JLINK下载更方便)新建工程模板——基于固件库新建工程模板——基于寄存器GPIO相关配置寄存器端口复用、重映射、中断JLINK在线调试JTAG/SWD调试原理软件仿真ST-LINK下载与调试程序(硬件)独立看门狗IWDG独立看门狗概述寄存器和库函数代码实现窗口看门狗WWDG窗口看门狗概述寄存器和库函数代码实现RTC 实时时钟STM32F1 RTC 时钟简介20.2 硬件设计20.3 软件设计20.4 下载验证待机睡眠唤醒(低功耗)实验21.1 STM32 待机模式简介21.2 硬件设计21.3 软件设计21.4 下载与测试2.4G无线射频通信模块NRF24L01简介寄存器介绍硬件连接源码讲解SPI总线通信读写外部FLASHSPI 简介配置相关引脚的复用功能使能SPI2 时钟初始化SPI2,设置SPI2 工作模式使能SPI2SPI 传输数据查看SPI 传输状态W25Q128 FLASH介(数据手册)硬件设计软件设计下载验证485通信实验485接口原理FLASH 操作块bank/block两种叫法 扇区sector 页pageFlash相对于普通设备的特殊性FLASH 模拟 EEPROMSTM32 FLASH 简介FLASH操作(读、写、擦除)寄存器库函数(本质操作寄存器)硬件设计软件设计串口IAP实验(在程序中编程In Application Programming)IAP配置IAP 简介IAP运作流程APP 程序起始地址设置方法中断向量表的偏移量设置方法hex转bin文件总结硬件设计软件设计下载验证蓝牙模块HC05使用模块介绍模块AT指令集**与上位机串口通信****与手机蓝牙调试助手通信**与单片机连接通信STM32命名规则 STM32芯片解读 开发环境搭建(MDK - 就是ARM的keil,需破解 支持包 CH340串口驱动 JLINK驱动) 上图在MDK环境中点击下载即可或者去官网下载 程序下载方法 (ISP串口下载 JLINK下载更方便) ISP串口下载 以前学习的是电脑USB端连接CH340芯片。 正点原子还设计了一键下载电路不用来回拨动跳线帽。 JLINK程序下载 需要先安装JLINK驱动前面讲过了 在MDK中配置一下JLINK 新建工程模板——基于固件库 视频开发环境MDK5 固件库版本V3.5 解压正点原子文件夹内的固件库包–C:\Users\朱冠霖\Desktop\【正点原子】战舰STM32F103开发板\【正点原子】战舰STM32F103开发板资料 资料盘(A盘)\8STM32参考资料\1STM32F1xx固件库 新建工程模板——基于寄存器 GPIO相关配置寄存器 端口复用、重映射、中断 JLINK在线调试 JTAG/SWD调试原理 软件仿真 ST-LINK下载与调试程序(硬件) 独立看门狗IWDG 独立看门狗概述 寄存器和库函数 上图prer表示预分屏值 代码实现 wdg.h #ifndef __WDG_H #define __WDG_H #include sys.hvoid IWDG_Init(u8 prer,u16 rlr); void IWDG_Feed(void);#endifwdg.c #include wdg.h//初始化独立看门狗 //prer:分频数:0~7(只有低3位有效!) //分频因子4*2^prer.但最大值只能是256! //rlr:重装载寄存器值:低11位有效. //时间计算(大概):Tout((4*2^prer)*rlr)/40 (ms). void IWDG_Init(u8 prer,u16 rlr) { IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); //使能对寄存器IWDG_PR和IWDG_RLR的写操作(取消写保护)IWDG_SetPrescaler(prer); //设置IWDG预分频值:设置IWDG预分频值为64IWDG_SetReload(rlr); //设置IWDG重装载值rlrIWDG_ReloadCounter(); //按照IWDG重装载寄存器的值rlr重装载IWDG计数器IWDG_Enable(); //使能IWDG } //喂独立看门狗 void IWDG_Feed(void) { IWDG_ReloadCounter();//reload }main.c #include led.h #include delay.h #include key.h #include sys.h #include usart.h #include wdg.hint main(void) { delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级2位响应优先级uart_init(115200); //串口初始化为115200LED_Init(); //初始化与LED连接的硬件接口KEY_Init(); //按键初始化 delay_ms(500); //让人看得到灭IWDG_Init(4,625); //预分频数为64,重载值为625,溢出时间为1s LED00; //点亮LED0while(1){if(KEY_Scan(0)WKUP_PRES){ //调用函数IWDG_ReloadCounter()一样的 IWDG_Feed();//如果WK_UP按下,则喂狗灯一直亮 不喂狗则程序一直复位灯一直闪烁}delay_ms(10);}; }窗口看门狗WWDG 窗口看门狗概述 寄存器和库函数 代码实现 wdg.h #ifndef __WDG_H #define __WDG_H #include sys.hvoid IWDG_Init(u8 prer,u16 rlr); void IWDG_Feed(void);void WWDG_Init(u8 tr,u8 wr,u32 fprer);//初始化WWDG void WWDG_Set_Counter(u8 cnt); //设置WWDG的计数器 void WWDG_NVIC_Init(void); #endifwdg.c #include wdg.h #include led.hvoid IWDG_Init(u8 prer,u16 rlr) { IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); //使能对寄存器IWDG_PR和IWDG_RLR的写操作IWDG_SetPrescaler(prer); //设置IWDG预分频值:设置IWDG预分频值为64IWDG_SetReload(rlr); //设置IWDG重装载值IWDG_ReloadCounter(); //按照IWDG重装载寄存器的值重装载IWDG计数器 IWDG_Enable(); //使能IWDG } //喂独立看门狗 void IWDG_Feed(void) { IWDG_ReloadCounter(); //重载计数值 }//保存WWDG计数器的设置值,默认为最大. u8 WWDG_CNT0x7f; //初始化窗口看门狗 //tr :T[6:0],计数器值 往下计数 //wr :W[6:0],窗口值 //fprer:分频系数WDGTB,仅最低2位有效 //FwwdgPCLK1/(4096*2^fprer). void WWDG_Init(u8 tr,u8 wr,u32 fprer) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE); // WWDG时钟使能WWDG_CNTtrWWDG_CNT; //初始化WWDG_CNT. 与运算取后7位 WWDG_SetPrescaler(fprer);设置IWDG预分频值WWDG_SetWindowValue(wr);//设置窗口值WWDG_Enable(WWDG_CNT); //使能看门狗 , 设置 counter . WWDG_ClearFlag();//清除提前唤醒中断标志位 WWDG_NVIC_Init();//初始化窗口看门狗 NVICWWDG_EnableIT(); //开启窗口看门狗中断 } //重设置WWDG计数器的值 void WWDG_Set_Counter(u8 cnt) {WWDG_Enable(cnt);//使能看门狗 , 设置 counter . }//窗口看门狗中断服务程序 void WWDG_NVIC_Init() {NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel WWDG_IRQn; //WWDG中断NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 2; //抢占2子优先级3组2 NVIC_InitStructure.NVIC_IRQChannelSubPriority 3; //抢占2子优先级3组2 NVIC_InitStructure.NVIC_IRQChannelCmdENABLE; NVIC_Init(NVIC_InitStructure);//NVIC初始化 }void WWDG_IRQHandler(void){WWDG_SetCounter(WWDG_CNT);//喂狗 当禁掉此句后,窗口看门狗将产生复位WWDG_ClearFlag(); //清除提前唤醒中断标志位LED1!LED1; //LED1状态翻转(指示喂狗成功)}main.c #include led.h #include delay.h #include key.h #include sys.h #include usart.h #include wdg.hint main(void){ delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组22位抢占优先级2位响应优先级uart_init(115200); //串口初始化为115200LED_Init();KEY_Init(); //按键初始化 LED00;delay_ms(300); //计数器从0x7F到0x40触发中断喂狗LED1灯翻转WWDG_Init(0X7F,0X5F,WWDG_Prescaler_8);//计数器值为7f,上窗口寄存器为5f(下窗口固定),分频数为8 while(1){LED01; } }RTC 实时时钟 前面我们介绍了两款液晶模块这一章我们将介绍STM32F1 的内部实时时钟RTC。在 本章中我们将利用ALIENTEK 2.8 寸TFTLCD 模块来显示日期和时间实现一个简单的时钟。 另外本章将顺带向大家介绍BKP(备份寄存器) 的使用。 STM32F1 RTC 时钟简介 STM32 的实时时钟RTC是一个独立的定时器。STM32 的RTC 模块拥有一组连续计数 的计数器在相应软件配置下可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。 RTC 模块和时钟配置系统(RCC_BDCR 寄存器)是在后备区域即在系统复位或从待机模式 唤醒后RTC 的设置和时间维持不变。但是在系统复位后会自动禁止访问后备寄存器和RTC以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前先要取消备份区域BKP写保护。 RTC 的简化框图如图20.1.1 所示 RTC 由两个主要部分组成参见图20.1.1第一部分(APB1 接口)用来和APB1 总线相连。此单元还包含一组16 位寄存器可通过APB1 总线对其进行读写操作。APB1 接口由APB1 总线时钟驱动用来与APB1 总线连接。通过APB1接口可以访问RTC的相关寄存器(预分频值计数器值、闹钟值)。 另一部分(RTC 核心)由一组可编程计数器组成分成两个主要模块。 第一个模块是RTC 的预分频模块它可编程产生1 秒的RTC 时间基准TR_CLK。RTC 的预分频模块包含了一个20位的可编程分频器(RTC 预分频器)。如果在RTC_CR 寄存器中设置了相应的允许位则在每个TR_CLK 周期中RTC 产生一个中断(秒中断)。 第二个模块是一个32 位的可编程计数器可被初始化为当前的系统时间一个32 位的时钟计数器按秒钟计算可以记录4294967296 秒约合136 年左右作为一般应用这已经是足够了的。 RTC 还有一个闹钟寄存器RTC_ALR用于产生闹钟。系统时间按TR_CLK 周期累加并与存储在RTC_ALR 寄存器中的可编程时间相比较如果RTC_CR 控制寄存器中设置了相应允许 位比较匹配时将产生一个闹钟中断。 RTC 内核完全独立于RTC APB1 接口而软件是通过APB1 接口访问RTC 的预分频值、计 数器值和闹钟值的。但是相关可读寄存器只在RTC APB1 时钟进行重新同步的RTC 时钟的上升沿被更新RTC 标志也是如此。这就意味着如果APB1 接口刚刚被开启之后在第一次的内部寄存器更新之前从APB1 上读取的RTC 寄存器值可能被破坏了通常读到0。因此若在读取RTC 寄存器曾经被禁止的RTC APB1 接口软件首先必须等待RTC_CRL 寄存器的RSF位寄存器同步标志位bit3被硬件置1。 要理解RTC 原理我们必须先通过对寄存器的讲解让大家有一个全面的了解。接下来 我们介绍一下RTC 相关的几个寄存器。 首先要介绍的是RTC 的控制寄存器RTC 总共有2 个控制寄存器RTC_CRH 和RTC_CRL两个都是16 位的。RTC_CRH 的各位描如图20.1.2 所示 该寄存器用来控制中断的我们本章将要用到秒钟中断所以在该寄存器必须设置最低位 为1以允许秒钟中断。 我们再看看RTC_CRL 寄存器。该寄存器各位描述如图20.1.3 所示 本章我们用到的是该寄存器的0、3~5 这几个位。 第0位是秒钟标志位我们在进入闹钟中断的时候通过判断这位来决定是不是发生了秒钟中断。然后必须通过软件将该位清零写0。第3 位为寄存器同步标志位我们在修改控制寄存器RTC_CRH/CRL 之前必须先判断该位是否已经同步了如果没有则等待同步在没同步的情况下修改RTC_CRH/CRL 的值是不行的。第4 位为配置标位在软件修改RTC_CNT/RTC_ALR/RTC_PRL 的值的时候必须先软件置位该位以允许进入配置模式。修改完之后设置CNF位为0退出配置模式。第5 位为RTC 操作位该位由硬件操作软件只读。通过该位可以判断上次对RTC 寄存器的操作是否完成如果没有我们必须等待上一次操作结束才能开始下一次操作。 第二个要介绍的寄存器是RTC 预分频装载寄存器也有2个寄存器组成RTC_PRLH 和 RTC_PRLL(分别代表高位和低位)。这两个寄存器用来配置RTC 时钟的分频数的比如我们使用外部32.768K 的晶振作为时钟的输入频率那么我们要设置这两个寄存器的值为32767以得到一秒钟的计数频率。 RTC_PRLH 的各位描述如图20.1.4 所示 从图20.1.4 可以看出RTC_PRLH 只有低四位有效用来存储PRL 的19~16 位。而PRL 的前16 位存放在RTC_PRLL 里面寄存器RTC_PRLL 的各位描述如图20.1.5 所示 在介绍完这两个寄存器之后我们介绍RTC 预分频器余数寄存器该寄存器也有2 个寄存器组成RTC_DIVH 和RTC_DIVL这两个寄存器的作用就是用来获得比秒钟更为准确的时钟比如可以得到0.1 秒或者0.01 秒等。该寄存器的值自减的用于保存还需要多少时钟周期获得一个秒信号。在一次秒钟更新后由硬件重新装载。这两个寄存器和RTC 预分频装载寄存器的各位是一样的这里我们就不列出来了。 接着要介绍的是RTC 最重要的寄存器RTC 计数器寄存器RTC_CNT。该寄存器由2 个16 位的寄存器组成RTC_CNTH 和RTC_CNTL总共32 位用来记录秒钟值一般情况下。此两个计数器也比较简单我们也不多说了。注意一点在修改这个寄存器的时候要先进入配置模式。 最后我们介绍RTC 部分的最后一个寄存器RTC 闹钟寄存器该寄存器也是由2 个16 为 的寄存器组成RTC_ALRH 和RTC_ALRL。总共也是32 位用来标记闹钟产生的时间以秒为单位如果RTC_CNT 的值与RTC_ALR 的值相等并使能了中断的话会产生一个闹钟中断。该寄存器的修改也要进入配置模式才能进行。 因为我们使用到备份寄存器(BKP)来存储RTC 的相关信息我们这里主要用来标记时钟是否已经经过了配置我们这里顺便介绍一下STM32 的备份寄存器。 备份寄存器是42 个16 位的寄存器战舰开发板就是大容量的可用来存储84 个字节的 用户应用程序数据。他们处在备份域里当VDD 电源被切断他们仍然由VBAT(纽扣电池) 维持供电。 即使系统在待机模式下被唤醒或系统复位或电源复位时他们也不会被复位。 此外BKP 控制寄存器用来管理侵入检测和RTC 校准功能这里我们不作介绍。 复位后对备份寄存器和RTC 的访问被禁止并且备份域被保护以防止可能存在的意外的 写操作。执行以下操作可以使能对备份寄存器和RTC 的访问 1通过设置寄存器RCC_APB1ENR 的PWREN 和BKPEN 位来打开电源和后备接口的时钟2电源控制寄存器(PWR_CR)的DBP 位来使能对后备寄存器和RTC 的访问。 我们一般用BKP 来存储RTC 的校验值或者记录一些重要的数据相当于一个EEPROM不过这个EEPROM 并不是真正的EEPROM而是需要电池来维持它的数据。关于BKP 的详细介绍请看《STM32 参考手册》的第47 页5.1 一节。 最后我们还要介绍一下备份区域控制寄存器RCC_BDCR。该寄存器的个位描述如图20.1.6所示 RTC 的时钟源选择及使能设置都是通过这个寄存器来实现的所以我们在RTC 操作之前 先要通过这个寄存器选择RTC 的时钟源然后才能开始其他的操作。 寄存器介绍就给大家介绍到这里了我们下面来看看要经过哪几个步骤的配置才能使RTC 正常工作这里我们将对每个步骤通过库函数的实现方式来讲解。 RTC 相关的库函数在文件stm32f10x_rtc.c 和stm32f10x_rtc.h 文件中BKP 相关的库函数在 文件stm32f10x_bkp.c 和文件stm32f10x_bkp.h 文件中。 RTC 正常工作的一般配置步骤如下 1使能电源时钟和备份区域时钟。 前面已经介绍了我们要访问RTC 和备份区域就必须先使能电源时钟和备份区域时钟。 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);2取消备份区写保护。 要向备份区域写入数据就要先取消备份区域写保护写保护在每次硬复位之后被使能 否则是无法向备份区域写入数据的。我们需要用到向备份区域写入一个字节来标记时钟已经配置过了这样避免每次复位之后重新配置时钟。取消备份区域写保护的库函数实现方法是 PWR_BackupAccessCmd(ENABLE); //使能RTC 和后备寄存器访问3复位备份区域开启外部低速振荡器。 在取消备份区域写保护之后我们可以先对这个区域复位以清除前面的设置当然这个 操作不要每次都执行因为备份区域的复位将导致之前存在的数据丢失所以要不要复位要看情况而定。然后我们使能外部低速振荡器注意这里一般要先判断RCC_BDCR 的LSERDY位来确定低速振荡器已经就绪了才开始下面的操作。 备份区域复位的函数是 BKP_DeInit();//复位备份区域开启外部低速振荡器的函数是 RCC_LSEConfig(RCC_LSE_ON);// 开启外部低速振荡器4选择RTC 时钟并使能。 这里我们将通过RCC_BDCR 的RTCSEL 来选择选择外部LSI 作为RTC 的时钟。然后通过 RTCEN 位使能RTC 时钟。 库函数中选择RTC 时钟的函数是 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择LSE 作为RTC 时钟对于RTC 时钟的选择还有RCC_RTCCLKSource_LSI 和RCC_RTCCLKSource_HSE_Div128两个顾名思义前者为LSI后者为HSE 的128 分频这在时钟系统章节有讲解过。 使能RTC 时钟的函数是 RCC_RTCCLKCmd(ENABLE); //使能RTC 时钟5设置RTC 的分频以及配置RTC 时钟。 在开启了RTC 时钟之后我们要做的就是设置RTC 时钟的分频数通过RTC_PRLH 和 RTC_PRLL 来设置然后等待RTC 寄存器操作完成并同步之后设置秒钟中断。然后设置RTC 的允许配置位RTC_CRH 的CNF 位设置时间其实就是设置RTC_CNTH 和RTC_CNTL两个寄存器。下面我们一一这些步骤用到的库函数 在进行RTC 配置之前首先要打开允许配置位(CNF)库函数是 RTC_EnterConfigMode();/// 允许配置在配置完成之后千万别忘记更新配置同时退出配置模式函数是 RTC_ExitConfigMode();//退出配置模式更新配置设置RTC 时钟分频数库函数是 void RTC_SetPrescaler(uint32_t PrescalerValue);这个函数只有一个入口参数就是RTC 时钟的分频数很好理解。 然后是设置秒中断允许RTC 使能中断的函数是 void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState)这个函数的第一个参数是设置秒中断类型这些通过宏定义定义的。对于使能秒中断方法是 RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC 秒中断下一步便是设置时间了设置时间实际上就是设置RTC 的计数值时间与计数值之间是需要换算的。库函数中设置RTC 计数值的方法是 void RTC_SetCounter(uint32_t CounterValue)//最后在配置完成之后通过这个函数直接设置RTC 计数值。 6更新配置设置RTC 中断分组。 在设置完时钟之后我们将配置更新同时退出配置模式这里还是通过RTC_CRH 的CNF 来实现。库函数的方法是 RTC_ExitConfigMode();//退出配置模式更新配置在退出配置模式更新配置之后我们在备份区域BKP_DR1 中写入0X5050 代表我们已经初始化过时钟了下次开机或复位的时候先读取BKP_DR1 的值然后判断是否是0X5050 来决定是不是要配置。接着我们配置RTC 的秒钟中断并进行分组。 往备份区域写用户数据的函数是 void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data)这个函数的第一个参数就是寄存器的标号了这个是通过宏定义定义的。比如我们要往 BKP_DR1 写入0x5050方法是 BKP_WriteBackupRegister(BKP_DR1, 0X5050);同时有写便有读读取备份区域指定寄存器的用户数据的函数是 uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR)这个函数就很好理解了这里不做过多讲解。 设置中断分组的方法之前已经详细讲解过调用NVIC_Init 函数即可这里不做重复讲解。 7编写中断服务函数。 最后我们要编写中断服务函数在秒钟中断产生的时候读取当前的时间值并显示到 TFTLCD 模块上。 通过以上几个步骤我们就完成了对RTC 的配置并通过秒钟中断来更新时间。接下来我 们将进行下一步的工作。 20.2 硬件设计 本实验用到的硬件资源有 指示灯DS0串口TFTLCD 模块RTC 前面3 个都介绍过了而RTC 属于STM32 内部资源其配置也是通过软件设置好就可以 了。不过RTC 不能断电否则数据就丢失了我们如果想让时间在断电后还可以继续走那么必须确保开发板的电池有电ALIENTEK 战舰STM32 开发板标配是有电池的。 20.3 软件设计 同样打开我们光盘的RTC 时钟实验可以看到我们的工程中加入了rtc.c 源文件和rtc.h 头文件同时引入了stm32f10x_rtc.c 和stm32f10x_bkp.c 库文件。 由于篇幅所限rtc.c 中的代码我们不全部贴出了这里针对几个重要的函数进行简要 说明首先是RTC_Init其代码如下 //实时时钟配置 //初始化RTC 时钟,同时检测时钟是否工作正常 //BKP-DR1 用于保存是否第一次配置的设置 //返回0:正常 //其他:错误代码 u8 RTC_Init(void) {u8 temp0;//检查是不是第一次配置时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR |RCC_APB1Periph_BKP, ENABLE); //①使能PWR 和BKP 外设时钟PWR_BackupAccessCmd(ENABLE); //②使能后备寄存器访问if (BKP_ReadBackupRegister(BKP_DR1) ! 0x5050) //从指定的后备寄存器中//读出数据:读出了与写入的指定数据不相乎{BKP_DeInit(); //③复位备份区域RCC_LSEConfig(RCC_LSE_ON); //设置外部低速晶振(LSE)while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) RESETtemp250)//检查指定的RCC 标志位设置与否,等待低速晶振就绪{temp;delay_ms(10);}if(temp250)return 1;//初始化时钟失败,晶振有问题RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //设置RTC 时钟//(RTCCLK),选择LSE 外部低速时钟 作为RTC时钟见下图所示RCC_RTCCLKCmd(ENABLE); //使能RTC 时钟RTC_WaitForLastTask(); //等待最近一次对RTC 寄存器的写操作完成RTC_WaitForSynchro(); //等待RTC 寄存器同步RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC 秒中断RTC_WaitForLastTask(); //等待最近一次对RTC 寄存器的写操作完成RTC_EnterConfigMode(); // 允许配置RTC_SetPrescaler(32767); //设置RTC 预分频的值RTC_WaitForLastTask(); //等待最近一次对RTC 寄存器的写操作完成RTC_Set(2015,1,14,17,42,55); //设置时间 下面介绍RTC_ExitConfigMode(); //退出配置模式BKP_WriteBackupRegister(BKP_DR1, 0X5050); //向指定的后备寄存器中//写入用户程序数据0x5050}else//系统继续计时{RTC_WaitForSynchro(); //等待最近一次对RTC 寄存器的写操作完成RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC 秒中断RTC_WaitForLastTask(); //等待最近一次对RTC 寄存器的写操作完成}RTC_NVIC_Config(); //RCT 中断分组设置RTC_Get(); //更新时间 下面介绍return 0; //ok }该函数用来初始化RTC 时钟但是只在第一次的时候设置时间以后如果重新上电/复位 都不会再进行时间设置了前提是备份电池有电在第一次配置的时候我们是按照上面介绍的RTC 初始化步骤来做的这里就不在多说了这里我们设置时间是通过时间设置函数RTC_Set函数来实现的该函数将在后续进行介绍。这里我们默认将时间设置为2015 年1 月14 日17点42 分55 秒。在设置好时间之后我们通过BKP_WriteBackupRegister()函数向BKP-DR1 写入标志字0X5050(这个值自己随便定的)用于标记时间已经被设置了。这样再次发生复位的时候该函数通过BKP_ReadBackupRegister()读取BKP-DR1 的值来判断决定是不是需要重新设置时间如果不需要设置则跳过时间设置仅仅使能秒钟中断一下就进行中断分组然后返回了。这样不会重复设置时间使得我们设置的时间不会因复位或者断电而丢失。 该函数还有返回值返回值代表此次操作的成功与否如果返回0则代表初始化RTC 成 功如果返回值非零则代表错误代码了。 介绍完RTC_Init我们来介绍一下RTC_Set 函数该函数代码如下 //设置时钟 //把输入的时钟转换为秒钟 //以1970 年1 月1 日为基准 //1970~2099 年为合法年份 //返回值:0,成功;其他:错误代码. //月份数据表 u8 const table_week[12]{0,3,3,6,1,4,6,2,5,0,3,5}; //月修正数据表 //平年的月份日期表 const u8 mon_table[12]{31,28,31,30,31,30,31,31,30,31,30,31}; u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec) {u16 t;u32 seccount0;if(syear1970||syear2099)return 1;for(t1970;tsyear;t) //把所有年份的秒钟相加{ if(Is_Leap_Year(t))seccount31622400;//闰年的秒钟数else seccount31536000; //平年的秒钟数}smon-1;for(t0;tsmon;t) //把前面月份的秒钟数相加{ seccount(u32)mon_table[t]*86400; //月份秒钟数相加if(Is_Leap_Year(syear)t1)seccount86400;//闰年2 月份增加一天的秒钟数}seccount(u32)(sday-1)*86400; //把前面日期的秒钟数相加seccount(u32)hour*3600; //小时秒钟数seccount(u32)min*60; //分钟秒钟数seccountsec; //最后的秒钟加上去RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR |RCC_APB1Periph_BKP, ENABLE); //使能PWR 和BKP 外设时钟PWR_BackupAccessCmd(ENABLE); //使能RTC 和后备寄存器访问RTC_SetCounter(seccount); //设置RTC 计数器的值RTC_WaitForLastTask(); //等待最近一次对RTC 寄存器的写操作完成return 0; }该函数用于设置时间把我们输入的时间转换为以1970 年1 月1 日0 时0 分0 秒当做起 始时间的秒钟信号后续的计算都以这个时间为基准的由于STM32 的秒钟计数器可以保存136 年的秒钟数据这样我们可以计时到2106 年。 接着我们介绍RTC_Alarm_Set 函数该函数用于设置闹钟时间同RTC_Set 函数几乎一 模一样主要区别就是将调用RTC_SetCounter 函数换成了调用RTC_SetAlarm 函数用于设置闹钟时间具体代码请参考本例程源码。 接着我们介绍一下RTC_Get 函数该函数用于获取时间和日期等数据其代码如下 //得到当前的时间结果保存在calendar 结构体里面 //返回值:0,成功;其他:错误代码. u8 RTC_Get(void) { static u16 daycnt0;u32 timecount0;u32 temp0;u16 temp10;timecountRTC-CNTH; //得到计数器中的值(秒钟数)timecount16;timecountRTC-CNTL;temptimecount/86400; //得到天数(秒钟数对应的)if(daycnt!temp) //超过一天了{daycnttemp;temp11970; //从1970 年开始while(temp365){if(Is_Leap_Year(temp1)) //是闰年{if(temp366)temp-366; //闰年的秒钟数else break;}else temp-365; //平年temp1;}calendar.w_yeartemp1; //得到年份temp10;while(temp28) //超过了一个月{if(Is_Leap_Year(calendar.w_year)temp11)//当年是不是闰年/2 月份{if(temp29)temp-29;//闰年的秒钟数else break;}else{ if(tempmon_table[temp1])temp-mon_table[temp1];//平年else break;}temp1;}calendar.w_monthtemp11; //得到月份calendar.w_datetemp1; //得到日期}temptimecount%86400; //得到秒钟数calendar.hourtemp/3600; //小时calendar.min(temp%3600)/60; //分钟calendar.sec(temp%3600)%60; //秒钟calendar.weekRTC_Get_Week(calendar.w_year,calendar.w_month,calendar.w_date);//获取星期return 0; } 函数其实就是将存储在秒钟寄存器RTC-CNTH 和RTC-CNTL 中的秒钟数据(通过函数 RTC_SetCounter 设置)转换为真正的时间和日期。该代码还用到了一个calendar 的结构体calendar 是我们在rtc.h 里面将要定义的一个时间结构体用来存放时钟的年月日时分秒等信息。 因为STM32 的RTC 只有秒钟计数器而年月日时分秒这些需要我们自己软件计算。我们把计算好的值保存在calendar 里面方便其他程序调用。 最后我们介绍一下秒钟中断服务函数该函数代码如下 //RTC 时钟中断 //每秒触发一次 void RTC_IRQHandler(void) {if (RTC_GetITStatus(RTC_IT_SEC) ! RESET) //秒钟中断{RTC_Get(); //更新时间}if(RTC_GetITStatus(RTC_IT_ALR)! RESET) //闹钟中断{RTC_ClearITPendingBit(RTC_IT_ALR); //清闹钟中断RTC_Get(); //更新时间printf(Alarm Time:%d-%d-%d %d:%d:%d\n,calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour,calendar.min,calendar.sec);//输出闹铃时间}RTC_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW); //清闹钟中断RTC_WaitForLastTask(); } 此部分代码比较简单我们通过RTC_GetITStatus 来判断发生的是何种中断如果是秒钟 中断则执行一次时间的计算获得最新时间结果保存在calendar 结构体里面因此我们可以在calendar 里面读到最新的时间、日期等信息。如果是闹钟中断则更新时间后将当前的闹铃时间通过printf 打印出来可以在串口调试助手看到当前的闹铃情况。 rtc.c 的其他程序这里就不再介绍了请大家直接看光盘的源码。接下来看看rtc.h 代码 在rtc.h 中我们定义了一个结构体 typedef struct {vu8 hour;vu8 min;vu8 sec;//公历日月年周vu16 w_year;vu8 w_month;vu8 w_date;vu8 week; }_calendar_obj;从上面结构体定义可以看到_calendar_obj 结构体所包含的成员变量是一个完整的公历信 息包括年、月、日、周、时、分、秒等7 个元素。我们以后要知道当前时间只需要通过RTC_Get函数执行时钟转换然后就可以从calendar 里面读出当前的公历时间了。 最后看看main.c 里面的代码如下 int main(void) {u8 t0;delay_init(); //延时函数初始化NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置NVIC 中断分组2uart_init(115200); //串口初始化波特率为115200LED_Init(); //LED 端口初始化LCD_Init(); //LCD 初始化usmart_dev.init(72); //初始化USMARTPOINT_COLORRED; //设置字体为红色LCD_ShowString(30,50,200,16,16,WarShip STM32F103 ^_^);LCD_ShowString(30,70,200,16,16,RTC TEST);LCD_ShowString(30,90,200,16,16,ATOMALIENTEK);LCD_ShowString(30,110,200,16,16,2015/1/14);while(RTC_Init()) //RTC 初始化一定要初始化成功{ LCD_ShowString(60,130,200,16,16,RTC ERROR! );delay_ms(800);LCD_ShowString(60,130,200,16,16,RTC Trying...);}//显示时间POINT_COLORBLUE; //设置字体为蓝色LCD_ShowString(60,130,200,16,16, - - );LCD_ShowString(60,162,200,16,16, : : );while(1){if(t!calendar.sec){tcalendar.sec;LCD_ShowNum(60,130,calendar.w_year,4,16);LCD_ShowNum(100,130,calendar.w_month,2,16);LCD_ShowNum(124,130,calendar.w_date,2,16);switch(calendar.week){case 0:LCD_ShowString(60,148,200,16,16,Sunday );break;case 1:LCD_ShowString(60,148,200,16,16,Monday );break;case 2:LCD_ShowString(60,148,200,16,16,Tuesday );break;case 3:LCD_ShowString(60,148,200,16,16,Wednesday);break;case 4:LCD_ShowString(60,148,200,16,16,Thursday );break;case 5:LCD_ShowString(60,148,200,16,16,Friday );break;case 6:LCD_ShowString(60,148,200,16,16,Saturday );break;}LCD_ShowNum(60,162,calendar.hour,2,16);LCD_ShowNum(84,162,calendar.min,2,16);LCD_ShowNum(108,162,calendar.sec,2,16);LED0!LED0;}delay_ms(10);}; }这部分代码就不再需要详细解释了在包含了rtc.h 之后通过判断calendar.sec 是否改变 来决定要不要更新时间显示。同时我们设置LED0 每2 秒钟闪烁一次用来提示程序已经开始跑了。 为了方便设置时间我们在usmart_config.c 里面修改usmart_nametab 如下 struct _m_usmart_nametab usmart_nametab[] { #if USMART_USE_WRFUNS1 //如果使能了读写操作(void*)read_addr,u32 read_addr(u32 addr),(void*)write_addr,void write_addr(u32 addr,u32 val), #endif(void*)delay_ms,void delay_ms(u16 nms),(void*)delay_us,void delay_us(u32 nus),(void*)RTC_Set,u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec), };将RTC_Set 加入了usmart同时去掉了上一章的设置减少代码量这样通过串口就可 以直接设置RTC 时间了。 至此RTC 实时时钟的软件设计就完成了接下来就让我们来检验一下我们的程序是否 正确了。 20.4 下载验证 将程序下载到战舰STM32 后可以看到DS0 不停的闪烁提示程序已经在运行了。同时 可以看到TFTLCD 模块开始显示时间实际显示效果如图20.4.1 所示 如果时间不正确大家可以用上一章介绍的方法通过串口调用RTC_Set 来设置一下当前 时间。 待机睡眠唤醒(低功耗)实验 本章我们将向大家介绍STM32F1 的待机唤醒功能。在本章中我们将利用WK_UP 按键 来实现唤醒和进入待机模式的功能然后利用DS0 指示状态。 21.1 STM32 待机模式简介 很多单片机都有低功耗模式STM32 也不例外。在系统或电源复位以后微控制器处于运 行状态。运行状态下的HCLK 为CPU 提供时钟内核执行程序代码。当CPU 不需继续运行时可以利用多个低功耗模式来节省功耗例如等待某个外部事件时。用户需要根据最低电源消耗最快速启动时间和可用的唤醒源等条件选定一个最佳的低功耗模式。STM32 的3 种低功耗模式我们在5.2.4 节有粗略介绍这里我们再回顾一下。 STM32 的低功耗模式有3 种 1)睡眠模式CM3 内核停止外设仍然运行2)停止模式所有时钟都停止3)待机模式1.8V 内核电源关闭 在运行模式下我们也可以通过降低系统时钟关闭APB 和AHB 总线上未被使用的外设的 时钟来降低功耗。三种低功耗模式一览表见表21.1.1 所示 在这三种低功耗模式中最低功耗的是待机模式在此模式下最低只需要2uA左右的电 流。停机模式是次低功耗的其典型的电流消耗在20uA左右。最后就是睡眠模式了。用户可以根据自己的需求来决定使用哪种低功耗模式。 本章我们仅对STM32 的最低功耗模式-待机模式来做介绍。待机模式可实现STM32 的最低功耗。该模式是在CM3 深睡眠模式时关闭电压调节器。整个1.8V 供电区域被断电。PLL、HSI 和HSE 振荡器也被断电。SRAM 和寄存器内容丢失。仅备份的寄存器和待机电路维持供电。 那么我们如何进入待机模式呢其实很简单只要按图21.1.1 所示的步骤执行就可以了 从图21.1.1 可知我们有4 种方式可以退出待机模式即当一个外部复位(NRST 引脚)、IWDG 复位、WKUP 引脚上的上升沿或RTC 闹钟事件发生时微控制器从待机模式退出。从待机唤醒后除了电源控制/状态寄存器(PWR_CSR)所有寄存器被复位。 从待机模式唤醒后的代码执行等同于复位后的执行(采样启动模式引脚读取复位向量等)。 电源控制/状态寄存器(PWR_CSR)将会指示内核由待机状态退出。 在进入待机模式后除了复位引脚以及被设置为防侵入或校准输出时的TAMPER 引脚和被 使能的唤醒引脚WK_UP 脚其他的IO 引脚都将处于高阻态。 图21.1.1 已经清楚的说明了进入待机模式的通用步骤其中涉及到2 个寄存器即电源控 制寄存器PWR_CR和电源控制/状态寄存器PWR_CSR。下面我们介绍一下这两个寄存器 电源控制寄存器PWR_CR该寄存器的各位描述如图21.1.2 所示 这里我们通过设置PWR_CR 的PDDS 位使CPU 进入深度睡眠时进入待机模式同时我 们通过CWUF 位清除之前的唤醒位。电源控制/状态寄存器PWR_CSR的各位描述如图21.1.3所示 这里我们通过设置PWR_CSR 的EWUP 位来使能WKUP 引脚用于待机模式唤醒。我 们还可以从WUF 来检查是否发生了唤醒事件。不过本章我们并没有用到。 通过以上介绍我们了解了进入待机模式的方法以及设置WK_UP 引脚用于把STM32 从待机模式唤醒的方法。具体步骤如下 1使能电源时钟。 因为要配置电源控制寄存器所以必须先使能电源时钟。 在库函数中使能电源时钟的方法是 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //使能PWR 外设时钟这个函数非常容易理解。 2) 设置WK_UP 引脚作为唤醒源。 使能时钟之后后再设置PWR_CSR 的EWUP 位使能WK_UP 用于将CPU 从待机模式唤 醒。在库函数中设置使能WK_UP 用于唤醒CPU 待机模式的函数是 PWR_WakeUpPinCmd(ENABLE); //使能唤醒管脚功能3设置SLEEPDEEP 位设置PDDS 位执行WFI 指令进入待机模式。 进入待机模式首先要设置SLEEPDEEP 位该位在系统控制寄存器SCB_SCR的第 二位详见《CM3 权威指南》第182 页表13.1接着我们通过PWR_CR 设置PDDS 位使得CPU 进入深度睡眠时进入待机模式最后执行WFI 指令开始进入待机模式并等待WK_UP中断的到来。在库函数中进行上面三个功能进入待机模式是在函数PWR_EnterSTANDBYMode中实现的(一个库函数搞定) void PWR_EnterSTANDBYMode(void)4最后编写WK_UP 中断函数。 因为我们通过WK_UP 中断PA0 中断来唤醒CPU所以我们有必要设置一下该中断函 数同时我们也通过该函数里面进入待机模式。 通过以上几个步骤的设置我们就可以使用STM32 的待机模式了并且可以通过WK_UP 来唤醒CPU我们最终要实现这样一个功能通过长按3 秒WK_UP 按键开机并且通过DS0 的闪烁指示程序已经开始运行再次长按该键则进入待机模式DS0 关闭程序停止运行。类似于手机的开关机。 21.2 硬件设计 本实验用到的硬件资源有 指示灯DS0WK_UP 按键 本章我们使用了WK_UP 按键用于唤醒和进入待机模式。然后通过DS0 来指示程序是否 在运行。这两个硬件的连接前面均有介绍。 21.3 软件设计 打开待机唤醒实验工程我们可以发现工程中多了一个wkup.c 和wkup.h 文件相关的用 户代码写在这两个文件中。同时对于待机唤醒功能我们需要引入stm32f10x_pwr.c 和 stm32f0x_pwr.h 文件。 打开wkup.c可以看到如下关键代码 void Sys_Standby(void) {RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //使能PWR 外设时钟PWR_WakeUpPinCmd(ENABLE); //使能唤醒管脚功能PWR_EnterSTANDBYMode(); //进入待命STANDBY模式 } //系统进入待机模式 void Sys_Enter_Standby(void) {RCC_APB2PeriphResetCmd(0X01FC,DISABLE); //复位所有IO 口Sys_Standby(); }//检测WKUP 脚的信号 //返回值1:连续按下3s 以上 // 0:错误的触发 u8 Check_WKUP(void) {u8 t0; //记录按下的时间LED00; //亮灯DS0while(1){if(WKUP_KD){t; //已经按下了delay_ms(30);if(t100) //按下超过3 秒钟{LED00; //点亮DS0return 1; //按下3s 以上了}}else{LED01;return 0; //按下不足3 秒}} }//中断,检测到PA0 脚的一个上升沿. //中断线0 线上的中断检测 void EXTI0_IRQHandler(void) {EXTI_ClearITPendingBit(EXTI_Line0); // 清除LINE10 上的中断标志位if(Check_WKUP()) //关机?{Sys_Enter_Standby();} }//PA0 WKUP 唤醒初始化 void WKUP_Init(void) { GPIO_InitTypeDef GPIO_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;EXTI_InitTypeDef EXTI_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_AFIO, ENABLE); //使能GPIOA 和复用功能时钟GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; //PA.0GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPD; //上拉输入GPIO_Init(GPIOA, GPIO_InitStructure); //初始化IO//使用外部中断方式GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);//中断线0 连接GPIOA.0EXTI_InitStructure.EXTI_Line EXTI_Line0; //设置按键所有的外部线路EXTI_InitStructure.EXTI_Mode EXTI_Mode_Interrupt; //外部中断模式EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Rising; //上升沿触发EXTI_InitStructure.EXTI_LineCmd ENABLE;EXTI_Init(EXTI_InitStructure); // 初始化外部中断NVIC_InitStructure.NVIC_IRQChannel EXTI0_IRQn; //使能外部中断通道NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 2; //先占优先级2 级NVIC_InitStructure.NVIC_IRQChannelSubPriority 2; //从优先级2 级NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; //外部中断通道使能NVIC_Init(NVIC_InitStructure); //初始化NVICif(Check_WKUP()0) Sys_Standby(); //不是开机,进入待机模式 }注意这个项目中不要同时引用exit.c 文件因为exit.c 里面也有一个void EXTI0_IRQHandler(void)函数如果不删除MDK 就会报错。该部分代码比较简单我们在这里说明两点 1在void Sys_Enter_Standby(void)函数里面我们要在进入待机模式前把所有开启的外设 全部关闭我们这里仅仅复位了所有的IO 口使得IO 口全部为浮空输入。其他外设比如 ADC 等大家根据自己所开启的情况进行一一关闭就可这样才能达到最低功耗 2在void WKUP_Init(void)函数里面我们要先判断WK_UP 是否按下了3 秒钟来决定 要不要开机如果没有按下3 秒钟程序直接就进入了待机模式。所以在下载完代码的时候是看不到任何反应的。我们必须先按WK_UP 按键3 秒钟以开机才能看到DS0 闪烁。 wkup.h 头文件的代码非常简单这里我们就不列出来。最后我们看看main.c 里面main 函 数代码如下 int main(void) {delay_init(); //延时函数初始化NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC 中断分组2uart_init(115200); //串口初始化波特率为115200LED_Init(); //LED 端口初始化WKUP_Init(); //待机唤醒初始化LCD_Init(); //LCD 初始化POINT_COLORRED;LCD_ShowString(30,50,200,16,16,Warship STM32);LCD_ShowString(30,70,200,16,16,WKUP TEST);LCD_ShowString(30,90,200,16,16,ATOMALIENTEK);LCD_ShowString(30,110,200,16,16,2014/1/14);while(1){ LED0!LED0;delay_ms(250);} }这里我们先初始化LED 和WK_UP 按键通过WKUP_Init函数初始化如果检测到 有长按WK_UP 按键3 秒以上则开机并执行LCD 初始化在LCD 上面显示一些内容如 果没有长按则在WKUP_Init 里面调用Sys_Enter_Standby 函数直接进入待机模式了。 开机后在死循环里面等待WK_UP 中断的到来在得到中断后在中断函数里面判断 WK_UP 按下的时间长短来决定是否进入待机模式如果按下时间超过3 秒则进入待机否则退出中断继续执行main 函数的死循环等待同时不停的取反LED0让红灯闪烁。 代码部分就介绍到这里大家记住下载代码后一定要长按WK_UP 按键来开机否则 将直接进入待机模式无任何现象。 21.4 下载与测试 在代码编译成功之后下载代码到战舰STM32 V3 上此时看不到任何现象和没下载 代码一样其实这是正常的在程序下载完之后开发板检测不到WK_UP即WK_UP 按键的持续按下3 秒以上所以直接进入待机模式看起来和没有下载代码一样。然后我们长按WK_UP 按键3 秒钟左右WK_UP 按下时DS0 会长亮可以看到DS0 开始闪烁液晶也会显示一些内容。然后再长按WK_UPDS0 会灭掉液晶灭掉程序再次进入待机模式。 2.4G无线射频通信模块 NRF24L01简介 寄存器介绍 硬件连接 源码讲解 24101.h #ifndef __24L01_H #define __24L01_H #include sys.h // //本程序只供学习使用未经作者许可不得用于其它任何用途 //ALIENTEK战舰STM32开发板V3 //NRF24L01驱动代码 //正点原子ALIENTEK //技术论坛:www.openedv.com //创建日期:2015/1/17 //版本V1.0 //版权所有盗版必究。 //Copyright(C) 广州市星翼电子科技有限公司 2009-2019 //All rights reserved //// //NRF24L01寄存器操作命令 #define NRF_READ_REG 0x00 //读配置寄存器,低5位为寄存器地址 #define NRF_WRITE_REG 0x20 //写配置寄存器,低5位为寄存器地址 #define RD_RX_PLOAD 0x61 //读RX有效数据,1~32字节 #define WR_TX_PLOAD 0xA0 //写TX有效数据,1~32字节 #define FLUSH_TX 0xE1 //清除TX FIFO寄存器.发射模式下用 #define FLUSH_RX 0xE2 //清除RX FIFO寄存器.接收模式下用 #define REUSE_TX_PL 0xE3 //重新使用上一包数据,CE为高,数据包被不断发送. #define NOP 0xFF //空操作,可以用来读状态寄存器 //SPI(NRF24L01)寄存器地址 #define CONFIG 0x00 //配置寄存器地址;bit0:1接收模式,0发射模式;bit1:电选择;bit2:CRC模式;bit3:CRC使能;//bit4:中断MAX_RT(达到最大重发次数中断)使能;bit5:中断TX_DS使能;bit6:中断RX_DR使能 #define EN_AA 0x01 //使能自动应答功能 bit0~5,对应通道0~5 #define EN_RXADDR 0x02 //接收地址允许,bit0~5,对应通道0~5 #define SETUP_AW 0x03 //设置地址宽度(所有数据通道):bit1,0:00,3字节;01,4字节;02,5字节; #define SETUP_RETR 0x04 //建立自动重发;bit3:0,自动重发计数器;bit7:4,自动重发延时 250*x86us #define RF_CH 0x05 //RF通道,bit6:0,工作通道频率; #define RF_SETUP 0x06 //RF寄存器;bit3:传输速率(0:1Mbps,1:2Mbps);bit2:1,发射功率;bit0:低噪声放大器增益 #define STATUS 0x07 //状态寄存器;bit0:TX FIFO满标志;bit3:1,接收数据通道号(最大:6);bit4,达到最多次重发//bit5:数据发送完成中断;bit6:接收数据中断; #define MAX_TX 0x10 //达到最大发送次数中断 #define TX_OK 0x20 //TX发送完成中断 #define RX_OK 0x40 //接收到数据中断#define OBSERVE_TX 0x08 //发送检测寄存器,bit7:4,数据包丢失计数器;bit3:0,重发计数器 #define CD 0x09 //载波检测寄存器,bit0,载波检测; #define RX_ADDR_P0 0x0A //数据通道0接收地址,最大长度5个字节,低字节在前 #define RX_ADDR_P1 0x0B //数据通道1接收地址,最大长度5个字节,低字节在前 #define RX_ADDR_P2 0x0C //数据通道2接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等; #define RX_ADDR_P3 0x0D //数据通道3接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等; #define RX_ADDR_P4 0x0E //数据通道4接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等; #define RX_ADDR_P5 0x0F //数据通道5接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等; #define TX_ADDR 0x10 //发送地址(低字节在前),ShockBurstTM模式下,RX_ADDR_P0与此地址相等 #define RX_PW_P0 0x11 //接收数据通道0有效数据宽度(1~32字节),设置为0则非法 #define RX_PW_P1 0x12 //接收数据通道1有效数据宽度(1~32字节),设置为0则非法 #define RX_PW_P2 0x13 //接收数据通道2有效数据宽度(1~32字节),设置为0则非法 #define RX_PW_P3 0x14 //接收数据通道3有效数据宽度(1~32字节),设置为0则非法 #define RX_PW_P4 0x15 //接收数据通道4有效数据宽度(1~32字节),设置为0则非法 #define RX_PW_P5 0x16 //接收数据通道5有效数据宽度(1~32字节),设置为0则非法 #define NRF_FIFO_STATUS 0x17 //FIFO状态寄存器;bit0,RX FIFO寄存器空标志;bit1,RX FIFO满标志;bit2,3,保留//bit4,TX FIFO空标志;bit5,TX FIFO满标志;bit6,1,循环发送上一数据包.0,不循环; // //24L01操作线 #define NRF24L01_CE PGout(8) //24L01片选信号 #define NRF24L01_CSN PGout(7) //SPI片选信号 #define NRF24L01_IRQ PGin(6) //IRQ主机数据输入 //24L01发送接收数据宽度定义 #define TX_ADR_WIDTH 5 //5字节的地址宽度 #define RX_ADR_WIDTH 5 //5字节的地址宽度 #define TX_PLOAD_WIDTH 32 //32字节的用户数据宽度 #define RX_PLOAD_WIDTH 32 //32字节的用户数据宽度void NRF24L01_Init(void); //初始化 void NRF24L01_RX_Mode(void); //配置为接收模式 void NRF24L01_TX_Mode(void); //配置为发送模式 u8 NRF24L01_Write_Buf(u8 reg, u8 *pBuf, u8 u8s);//写数据区 u8 NRF24L01_Read_Buf(u8 reg, u8 *pBuf, u8 u8s); //读数据区 u8 NRF24L01_Read_Reg(u8 reg); //读寄存器 u8 NRF24L01_Write_Reg(u8 reg, u8 value); //写寄存器 u8 NRF24L01_Check(void); //检查24L01是否存在 u8 NRF24L01_TxPacket(u8 *txbuf); //发送一个包的数据 u8 NRF24L01_RxPacket(u8 *rxbuf); //接收一个包的数据 #endif24101.c #include 24l01.h #include lcd.h #include delay.h #include spi.h #include usart.h // //本程序只供学习使用未经作者许可不得用于其它任何用途 //ALIENTEK战舰STM32开发板 //NRF24L01驱动代码 //正点原子ALIENTEK //技术论坛:www.openedv.com //修改日期:2012/9/13 //版本V1.0 //版权所有盗版必究。 //Copyright(C) 广州市星翼电子科技有限公司 2009-2019 //All rights reserved //const u8 TX_ADDRESS[TX_ADR_WIDTH]{0x34,0x43,0x10,0x10,0x01}; //发送地址 const u8 RX_ADDRESS[RX_ADR_WIDTH]{0x34,0x43,0x10,0x10,0x01};//初始化24L01的IO口 void NRF24L01_Init(void) { GPIO_InitTypeDef GPIO_InitStructure;SPI_InitTypeDef SPI_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOG, ENABLE); //使能PB,G端口时钟GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; //PB12上拉 防止W25X的干扰GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; //推挽输出GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOB, GPIO_InitStructure); //初始化指定IOGPIO_SetBits(GPIOB,GPIO_Pin_12);//上拉 GPIO_InitStructure.GPIO_Pin GPIO_Pin_7|GPIO_Pin_8; //PG8 7 推挽 GPIO_Init(GPIOG, GPIO_InitStructure);//初始化指定IOGPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPD; //PG6 输入 GPIO_Init(GPIOG, GPIO_InitStructure);GPIO_ResetBits(GPIOG,GPIO_Pin_6|GPIO_Pin_7|GPIO_Pin_8);//PG6,7,8上拉 SPI2_Init(); //初始化SPI SPI_Cmd(SPI2, DISABLE); // SPI外设不使能SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; //SPI设置为双线双向全双工SPI_InitStructure.SPI_Mode SPI_Mode_Master; //SPI主机SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; //发送接收8位帧结构SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; //时钟悬空低SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; //数据捕获于第1个时钟沿SPI_InitStructure.SPI_NSS SPI_NSS_Soft; //NSS信号由软件控制SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_16; //定义波特率预分频的值:波特率预分频值为16SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; //数据传输从MSB位开始SPI_InitStructure.SPI_CRCPolynomial 7; //CRC值计算的多项式SPI_Init(SPI2, SPI_InitStructure); //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器SPI_Cmd(SPI2, ENABLE); //使能SPI外设NRF24L01_CE0; //使能24L01NRF24L01_CSN1; //SPI片选取消 } //检测24L01是否存在 //返回值:0成功;1失败 u8 NRF24L01_Check(void) {u8 buf[5]{0XA5,0XA5,0XA5,0XA5,0XA5};u8 i;SPI2_SetSpeed(SPI_BaudRatePrescaler_4); //spi速度为9Mhz24L01的最大SPI时钟为10Mhz NRF24L01_Write_Buf(NRF_WRITE_REGTX_ADDR,buf,5);//写入5个字节的地址. NRF24L01_Read_Buf(TX_ADDR,buf,5); //读出写入的地址 for(i0;i5;i)if(buf[i]!0XA5)break; if(i!5)return 1;//检测24L01错误 return 0; //检测到24L01 } //SPI写寄存器 //reg:指定寄存器地址 //value:写入的值 u8 NRF24L01_Write_Reg(u8 reg,u8 value) {u8 status; NRF24L01_CSN0; //使能SPI传输status SPI2_ReadWriteByte(reg);//发送寄存器号 SPI2_ReadWriteByte(value); //写入寄存器的值NRF24L01_CSN1; //禁止SPI传输 return(status); //返回状态值 } //读取SPI寄存器值 //reg:要读的寄存器 u8 NRF24L01_Read_Reg(u8 reg) {u8 reg_val; NRF24L01_CSN 0; //使能SPI传输 SPI2_ReadWriteByte(reg); //发送寄存器号reg_valSPI2_ReadWriteByte(0XFF);//读取寄存器内容NRF24L01_CSN 1; //禁止SPI传输 return(reg_val); //返回状态值 } //在指定位置读出指定长度的数据 //reg:寄存器(位置) //*pBuf:数据指针 //len:数据长度 //返回值,此次读到的状态寄存器值 u8 NRF24L01_Read_Buf(u8 reg,u8 *pBuf,u8 len) {u8 status,u8_ctr; NRF24L01_CSN 0; //使能SPI传输statusSPI2_ReadWriteByte(reg);//发送寄存器值(位置),并读取状态值 for(u8_ctr0;u8_ctrlen;u8_ctr)pBuf[u8_ctr]SPI2_ReadWriteByte(0XFF);//读出数据NRF24L01_CSN1; //关闭SPI传输return status; //返回读到的状态值 } //在指定位置写指定长度的数据 //reg:寄存器(位置) //*pBuf:数据指针 //len:数据长度 //返回值,此次读到的状态寄存器值 u8 NRF24L01_Write_Buf(u8 reg, u8 *pBuf, u8 len) {u8 status,u8_ctr; NRF24L01_CSN 0; //使能SPI传输status SPI2_ReadWriteByte(reg);//发送寄存器值(位置),并读取状态值for(u8_ctr0; u8_ctrlen; u8_ctr)SPI2_ReadWriteByte(*pBuf); //写入数据 NRF24L01_CSN 1; //关闭SPI传输return status; //返回读到的状态值 } //启动NRF24L01发送一次数据 //txbuf:待发送数据首地址 //返回值:发送完成状况 u8 NRF24L01_TxPacket(u8 *txbuf) {u8 sta;SPI2_SetSpeed(SPI_BaudRatePrescaler_8);//spi速度为9Mhz24L01的最大SPI时钟为10Mhz NRF24L01_CE0;NRF24L01_Write_Buf(WR_TX_PLOAD,txbuf,TX_PLOAD_WIDTH);//写数据到TX BUF 32个字节NRF24L01_CE1;//启动发送 while(NRF24L01_IRQ!0);//等待发送完成staNRF24L01_Read_Reg(STATUS); //读取状态寄存器的值 NRF24L01_Write_Reg(NRF_WRITE_REGSTATUS,sta); //清除TX_DS或MAX_RT中断标志if(staMAX_TX)//达到最大重发次数{NRF24L01_Write_Reg(FLUSH_TX,0xff);//清除TX FIFO寄存器 return MAX_TX; }if(staTX_OK)//发送完成{return TX_OK;}return 0xff;//其他原因发送失败 } //启动NRF24L01发送一次数据 //txbuf:待发送数据首地址 //返回值:0接收完成其他错误代码 u8 NRF24L01_RxPacket(u8 *rxbuf) {u8 sta; SPI2_SetSpeed(SPI_BaudRatePrescaler_8); //spi速度为9Mhz24L01的最大SPI时钟为10Mhz staNRF24L01_Read_Reg(STATUS); //读取状态寄存器的值 NRF24L01_Write_Reg(NRF_WRITE_REGSTATUS,sta); //清除TX_DS或MAX_RT中断标志if(staRX_OK)//接收到数据{NRF24L01_Read_Buf(RD_RX_PLOAD,rxbuf,RX_PLOAD_WIDTH);//读取数据NRF24L01_Write_Reg(FLUSH_RX,0xff);//清除RX FIFO寄存器 return 0; } return 1;//没收到任何数据 } //该函数初始化NRF24L01到RX模式 //设置RX地址,写RX数据宽度,选择RF频道,波特率和LNA HCURR //当CE变高后,即进入RX模式,并可以接收数据了 void NRF24L01_RX_Mode(void) {NRF24L01_CE0; NRF24L01_Write_Buf(NRF_WRITE_REGRX_ADDR_P0,(u8*)RX_ADDRESS,RX_ADR_WIDTH);//写RX节点地址NRF24L01_Write_Reg(NRF_WRITE_REGEN_AA,0x01); //使能通道0的自动应答 NRF24L01_Write_Reg(NRF_WRITE_REGEN_RXADDR,0x01);//使能通道0的接收地址 NRF24L01_Write_Reg(NRF_WRITE_REGRF_CH,40); //设置RF通信频率 NRF24L01_Write_Reg(NRF_WRITE_REGRX_PW_P0,RX_PLOAD_WIDTH);//选择通道0的有效数据宽度 NRF24L01_Write_Reg(NRF_WRITE_REGRF_SETUP,0x0f);//设置TX发射参数,0db增益,2Mbps,低噪声增益开启 NRF24L01_Write_Reg(NRF_WRITE_REGCONFIG, 0x0f);//配置基本工作模式的参数;PWR_UP,EN_CRC,16BIT_CRC,接收模式 NRF24L01_CE 1; //CE为高,进入接收模式 } //该函数初始化NRF24L01到TX模式 //设置TX地址,写TX数据宽度,设置RX自动应答的地址,填充TX发送数据,选择RF频道,波特率和LNA HCURR //PWR_UP,CRC使能 //当CE变高后,即进入RX模式,并可以接收数据了 //CE为高大于10us,则启动发送. void NRF24L01_TX_Mode(void) { NRF24L01_CE0; NRF24L01_Write_Buf(NRF_WRITE_REGTX_ADDR,(u8*)TX_ADDRESS,TX_ADR_WIDTH);//写TX节点地址 NRF24L01_Write_Buf(NRF_WRITE_REGRX_ADDR_P0,(u8*)RX_ADDRESS,RX_ADR_WIDTH); //设置TX节点地址,主要为了使能ACK NRF24L01_Write_Reg(NRF_WRITE_REGEN_AA,0x01); //使能通道0的自动应答 NRF24L01_Write_Reg(NRF_WRITE_REGEN_RXADDR,0x01); //使能通道0的接收地址 NRF24L01_Write_Reg(NRF_WRITE_REGSETUP_RETR,0x1a);//设置自动重发间隔时间:500us 86us;最大自动重发次数:10次NRF24L01_Write_Reg(NRF_WRITE_REGRF_CH,40); //设置RF通道为40NRF24L01_Write_Reg(NRF_WRITE_REGRF_SETUP,0x0f); //设置TX发射参数,0db增益,2Mbps,低噪声增益开启 NRF24L01_Write_Reg(NRF_WRITE_REGCONFIG,0x0e); //配置基本工作模式的参数;PWR_UP,EN_CRC,16BIT_CRC,接收模式,开启所有中断NRF24L01_CE1;//CE为高,10us后启动发送 }spi.c main.c #include led.h #include delay.h #include key.h #include sys.h #include lcd.h #include usart.h #include 24l01.h /************************************************ALIENTEK战舰STM32开发板实验33无线通信 实验技术支持www.openedv.com淘宝店铺http://eboard.taobao.com 关注微信公众平台微信号正点原子免费获取STM32资料。广州市星翼电子科技有限公司 作者正点原子 ALIENTEK ************************************************/int main(void){ u8 key,mode;u16 t0; u8 tmp_buf[33]; delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组22位抢占优先级2位响应优先级uart_init(115200); //串口初始化为115200LED_Init(); //初始化与LED连接的硬件接口KEY_Init(); //初始化按键LCD_Init(); //初始化LCD NRF24L01_Init(); //初始化NRF24L01 POINT_COLORRED; //设置字体为红色 LCD_ShowString(30,50,200,16,16,WarShip STM32); LCD_ShowString(30,70,200,16,16,NRF24L01 TEST); LCD_ShowString(30,90,200,16,16,ATOMALIENTEK);LCD_ShowString(30,110,200,16,16,2015/1/17); while(NRF24L01_Check()){LCD_ShowString(30,130,200,16,16,NRF24L01 Error);delay_ms(200);LCD_Fill(30,130,239,13016,WHITE);delay_ms(200);}LCD_ShowString(30,130,200,16,16,NRF24L01 OK); while(1){ keyKEY_Scan(0);if(keyKEY0_PRES){mode0; break;}else if(keyKEY1_PRES){mode1;break;}t;if(t100)LCD_ShowString(10,150,230,16,16,KEY0:RX_Mode KEY1:TX_Mode); //闪烁显示提示信息if(t200){ LCD_Fill(10,150,230,15016,WHITE);t0; }delay_ms(5); } LCD_Fill(10,150,240,166,WHITE);//清空上面的显示 POINT_COLORBLUE;//设置字体为蓝色 if(mode0)//RX模式{LCD_ShowString(30,150,200,16,16,NRF24L01 RX_Mode); LCD_ShowString(30,170,200,16,16,Received DATA:); NRF24L01_RX_Mode(); while(1){ if(NRF24L01_RxPacket(tmp_buf)0)//一旦接收到信息,则显示出来.{tmp_buf[32]0;//加入字符串结束符LCD_ShowString(0,190,lcddev.width-1,32,16,tmp_buf); }else delay_us(100); t;if(t10000)//大约1s钟改变一次状态{t0;LED0!LED0;} }; }else//TX模式{ LCD_ShowString(30,150,200,16,16,NRF24L01 TX_Mode); NRF24L01_TX_Mode();mode ;//从空格键开始 while(1){ if(NRF24L01_TxPacket(tmp_buf)TX_OK){LCD_ShowString(30,170,239,32,16,Sended DATA:); LCD_ShowString(0,190,lcddev.width-1,32,16,tmp_buf); keymode;for(t0;t32;t){key;if(key(~))key ;tmp_buf[t]key; }mode; if(mode~)mode ; tmp_buf[32]0;//加入结束符 }else{ LCD_Fill(0,170,lcddev.width,17016*3,WHITE);//清空显示 LCD_ShowString(30,170,lcddev.width-1,32,16,Send Failed ); };LED0!LED0;delay_ms(1500); };} }SPI总线通信读写外部FLASH SPI 简介 SPI 是英语Serial Peripheral interface 的缩写顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX 系列处理器上定义的。SPI 接口主要应用在EEPROMFLASH实时时钟AD 转换器还有数字信号处理器和数字信号解码器之间。 SPI是一种高速的全双工同步的通信总线并且在芯片的管脚上只占用四根线节约了芯片的管脚同时为PCB 的布局上节省空间提供方便正是出于这种简单易用的特性现在越来越多的芯片集成了这种通信协议STM32 也有SPI 接口。下面我们看看SPI 的内部简明图图29.1.1 SPI 接口一般使用4 条线通信 MISO 主设备数据输入从设备数据输出。MOSI 主设备数据输出从设备数据输入。SCLK 时钟信号由主设备产生。CS 从设备片选信号由主设备控制方便一个SPI接口上可以挂多个SPI设备片选引脚拉低即选中。 从图中可以看出主机和从机都有一个串行移位寄存器主机通过向它的SPI 串行寄存器写入一个字节来发起一次传输。寄存器通过MOSI 信号线将字节传送给从机从机也将自己的移位寄存器中的内容通过MISO 信号线返回给主机。这样两个移位寄存器中的内容就被交换。 外设的写操作和读操作是同步完成的。如果只进行写操作主机只需忽略接收到的字节反之若主机要读取从机的一个字节就必须发送一个空字节来引发从机的传输。 SPI 主要特点有可以同时发出和接收串行数据可以当作主机或从机工作提供频率可编程时钟发送结束中断标志写冲突保护总线竞争保护等。 SPI 总线四种工作方式SPI 模块为了和外设进行数据交换根据外设工作要求其输出串行同步时钟极性和相位可以进行配置时钟极性SPI_CR寄存器的CPOL位对传输协议没有重大的影响。如果CPOL0串行同步时钟的空闲状态为低电平如果CPOL1串行同步时钟的空闲状态为高电平。时钟相位SPI_CR寄存器的CPHA位能够配置用于选择两种不同的传输协议之一进行数据传输。如果CPHA0在串行同步时钟的第一个跳变沿上升或下降数据被采样如果CPHA1在串行同步时钟的第二个跳变沿上升或下降数据被采样。SPI 主模块和与之通信的外设备时钟相位和极性应该一致。 不同时钟相位下的总线数据传输时序如图29.1.2 所示 STM32 的SPI 功能很强大SPI 时钟最多可以到18Mhz支持DMA可以配置为SPI 协议或者I2S 协议仅大容量型号支持战舰STM32 开发板是支持的。 本章我们将利用STM32 的SPI 来读取外部SPI FLASH 芯片W25Q128实现类似上节的功能。这里对SPI 我们只简单介绍一下SPI 的使用STM32 的SPI 详细介绍请参考《STM32参考手册》第457 页23 节。然后我们再介绍下SPI FLASH 芯片。 这节我们使用STM32 的SPI2 的主模式下面就来看看SPI2 部分的设置步骤吧。SPI 相关的库函数和定义分布在文件stm32f10x_spi.c 以及头文件stm32f10x_spi.h 中。STM32 的主模式配置步骤如下 配置相关引脚的复用功能使能SPI2 时钟 我们要用SPI2第一步就要使能SPI2 的时钟。其次要设置SPI2 的相关引脚为复用输出这样才会连接到SPI2 上否则这些IO 口还是默认的状态也就是标准输入输出口。这里我们使用的是PB13、14、15 这3 个SCK.、MISO、MOSICS 使用软件管理方式所以设置这三个为复用IO。 GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE );//PORTB 时钟使能 RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE );//SPI2 时钟使能 GPIO_InitStructure.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; //PB13/14/15 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure);//初始化GPIOB类似的时钟使能和IO 初始化我们前面多次讲解到这里不做详细介绍。 初始化SPI2,设置SPI2 工作模式 接下来我们要初始化SPI2,设置SPI2 为主机模式设置数据格式为8 位然设置SCK 时钟极性及采样方式。并设置SPI2 的时钟频率最大18Mhz以及数据的格式MSB 在前还是LSB 在前。这在库函数中是通过SPI_Init 函数来实现的。 void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct)跟其他外设初始化一样第一个参数是SPI 标号这里我们是使用的SPI2。下面我们来看看第二个参数结构体类型SPI_InitTypeDef 的定义 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_Direction 是用来设置SPI 的通信方式可以选择为半双工全双工以及串行发和串行收方式这里我们选择全双工模式SPI_Direction_2Lines_FullDuplex。 第二个参数SPI_Mode 用来设置SPI 的主从模式这里我们设置为主机模式SPI_Mode_Master当然有需要你也可以选择为从机模式SPI_Mode_Slave。 第三个参数SPI_DataSiz 为8 位还是16 位帧格式选择项这里我们是8 位传输选择SPI_DataSize_8b。 第四个参数SPI_CPOL 用来设置时钟极性我们设置串行同步时钟的空闲状态为高电平所以我们选择SPI_CPOL_High。 第五个参数SPI_CPHA 用来设置时钟相位也就是选择在串行同步时钟的第几个跳变沿上升或下降数据被采样可以为第一个或者第二个条边沿采集这里我们选择第二个跳变沿所以选择SPI_CPHA_2Edge 第六个参数SPI_NSS 设置NSS 信号由硬件NSS 管脚还是软件控制这里我们通过软件控制NSS 关键而不是硬件自动控制所以选择SPI_NSS_Soft。 第七个参数SPI_BaudRatePrescaler 很关键就是设置SPI 波特率预分频值也就是决定SPI 的时钟的参数从不分频道256 分频8 个可选值初始化的时候我们选择256 分频值SPI_BaudRatePrescaler_256, 传输速度为36M/256140.625KHz。 第八个参数SPI_FirstBit 设置数据传输顺序是MSB 位在前还是LSB 位在前这里我们选择SPI_FirstBit_MSB 高位在前。 第九个参数SPI_CRCPolynomial 是用来设置CRC 校验多项式提高通信可靠性大于1 即可。 设置好上面9 个参数我们就可以初始化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 寄存器使能SPI2 初始化完成之后接下来是要使能SPI2 通信了在使能SPI2 之后我们就可以开始SPI 通讯了。使能SPI2 的方法是 SPI_Cmd(SPI2, ENABLE); //使能SPI 外设SPI 传输数据 通信接口当然需要有发送数据和接受数据的函数固件库提供的发送数据函数原型为 void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data)这个函数很好理解往SPIx 数据寄存器写入数据Data从而实现发送。 固件库提供的接受数据函数原型为 uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx) 这个函数也不难理解从SPIx 数据寄存器读出接受到的数据。 查看SPI 传输状态 在SPI 传输过程中我们经常要判断数据是否传输完成发送区是否为空等等状态这是通过函数SPI_I2S_GetFlagStatus 实现的这个函数很简单就不详细讲解判断发送是否完成的方法是 SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE)W25Q128 FLASH介(数据手册) SPI2 的使用就介绍到这里接下来介绍一下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.6VW25Q128 支持标准的SPI还支持双输出/四输出的SPI最大SPI 时钟可以到80Mhz双输出时相当于160Mhz四输出时相当于320M更多的W25Q128 的介绍请参考W25Q128 的DATASHEET。 硬件设计 本章实验功能简介开机的时候先检测W25Q128 是否存在然后在主循环里面检测两个按键其中1 个按键KEY1用来执行写入W25Q128 的操作另外一个按键KEY0用来执行读出操作在TFTLCD 模块上显示相关信息。同时用DS0 提示程序正在运行。所要用到的硬件资源如下 1指示灯DS0 2KEY0 和KEY1 按键 3TFTLCD 模块 4SPI 5W25Q128 这里只介绍W25Q128 与STM32 的连接板上的W25Q128 是直接连在STM32 的SPI2 上的连接关系如图29.2.1 所示 这里我们的F_CS 是连接在PB12 上面的另外要特别注意W25Q128 和NRF24L01 共用SPI2所以这两个器件在使用的时候必须分时复用通过片选控制才行。 软件设计 打开我们光盘的SPI 实验工程可以看到我们加入了spi.c,w25qxx.c 文件以及头文件spi.h和w25qxx.h同时引入了库函数文件stm32f10x_spi.c 文件以及头文件stm32f10x_spi.h。 打开spi.c 文件看到如下代码 #include spi.h//以下是SPI模块的初始化代码配置成主机模式访问SD Card/W25Q64/NRF24L01 //SPI口初始化 //这里针是对SPI2的初始化void SPI2_Init(void) {GPIO_InitTypeDef GPIO_InitStructure;SPI_InitTypeDef SPI_InitStructure;RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );//PORTB时钟使能 RCC_APB1PeriphClockCmd( RCC_APB1Periph_SPI2, ENABLE );//SPI2时钟使能 GPIO_InitStructure.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; //PB13/14/15复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOB, GPIO_InitStructure);//初始化GPIOBGPIO_SetBits(GPIOB,GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15); //PB13/14/15上拉SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工SPI_InitStructure.SPI_Mode SPI_Mode_Master; //设置SPI工作模式:设置为主SPISPI_InitStructure.SPI_DataSize SPI_DataSize_8b; //设置SPI的数据大小:SPI发送接收8位帧结构SPI_InitStructure.SPI_CPOL SPI_CPOL_High; //串行同步时钟的空闲状态为高电平SPI_InitStructure.SPI_CPHA SPI_CPHA_2Edge; //串行同步时钟的第二个跳变沿上升或下降数据被采样SPI_InitStructure.SPI_NSS SPI_NSS_Soft; //NSS信号由硬件NSS管脚还是软件使用SSI位管理:内部NSS信号有SSI位控制SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_256; //定义波特率预分频的值:波特率预分频值为256SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始SPI_InitStructure.SPI_CRCPolynomial 7; //CRC值计算的多项式SPI_Init(SPI2, SPI_InitStructure); //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器SPI_Cmd(SPI2, ENABLE); //使能SPI外设SPI2_ReadWriteByte(0xff);//启动传输 SPI通信规则:主机通过向它的SPI串行寄存器写入一个字节来发起一次传输} //SPI 速度设置函数 //SpeedSet: //SPI_BaudRatePrescaler_2 2分频 //SPI_BaudRatePrescaler_8 8分频 //SPI_BaudRatePrescaler_16 16分频 //SPI_BaudRatePrescaler_256 256分频 void SPI2_SetSpeed(u8 SPI_BaudRatePrescaler) {assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler));SPI2-CR10XFFC7;SPI2-CR1|SPI_BaudRatePrescaler; //设置SPI2速度 SPI_Cmd(SPI2,ENABLE); } //SPIx 读写一个字节 //TxData:要写入的字节 //返回值:读取到的字节 u8 SPI2_ReadWriteByte(u8 TxData) { u8 retry0; while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) RESET) //检查指定的SPI标志位设置与否:发送缓存空标志位{retry;if(retry200)return 0;} SPI_I2S_SendData(SPI2, TxData); //通过外设SPIx发送一个数据retry0;while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) RESET) //检查指定的SPI标志位设置与否:接受缓存非空标志位{retry;if(retry200)return 0;} return SPI_I2S_ReceiveData(SPI2); //返回通过SPIx最近接收的数据 }此部分代码主要初始化SPI这里我们选择的是SPI2所以在SPI2_Init 函数里面其相关的操作都是针对SPI2 的其初始化步骤和我们上面介绍步骤1-5 一样我们在代码中也使用了①~⑤标注。在初始化之后我们就可以开始使用SPI2 了在SPI2_Init 函数里面把SPI2 的波特率设置成了最低36Mhz256 分频为140.625KHz。在外部函数里面我们通过 SPI2_SetSpeed 来设置SPI2 的速度而我们的数据发送和接收则是通过SPI2_ReadWriteByte 函数来实现的。SPI2_SetSpeed 函数我们是通过寄存器设置方式来实现的因为固件库并没有提供单独的设置分频系数的函数当然我们也可以勉强的调用SPI_Init 初始化函数来实现分频系数修改。要读懂这段代码可以直接查找中文参考手册中SPI 章节的寄存器CR1 的描述即可。 这里特别注意SPI 初始化函数的最后有一个启动传输这句话最大的作用就是维持MOSI为高电平而且这句话也不是必须的可以去掉。 下面我们打开w25qxx.c里面编写的是与W25Q128 操作相关的代码由于篇幅所限详细代码这里就不贴出了。我们仅介绍几个重要的函数首先是W25QXX_Read 函数该函数用于从W25Q128 的指定地址读出指定长度的数据。其代码如下 //读取SPI FLASH //在指定地址开始读取指定长度的数据 //pBuffer:数据存储区 //ReadAddr:开始读取的地址(24bit) //NumByteToRead:要读取的字节数(最大65535) void W25QXX_Read (u8* pBuffer,u32 ReadAddr,u16 NumByteToRead) {u16 i;SPI_FLASH_CS0; //使能器件SPI2_ReadWriteByte(W25X_ReadData); //发送读取命令SPI2_ReadWriteByte((u8)((ReadAddr)16)); //发送24bit 地址SPI2_ReadWriteByte((u8)((ReadAddr)8));SPI2_ReadWriteByte((u8)ReadAddr);for(i0;iNumByteToRead;i){pBuffer[i]SPI2_ReadWriteByte(0XFF); //循环读数}SPI_FLASH_CS1; }由于W25Q128 支持以任意地址但是不能超过W25Q128 的地址范围开始读取数据所以这个代码相对来说就比较简单了在发送24 位地址之后程序就可以开始循环读数据了其地址会自动增加的不过要注意不能读的数据超过了W25Q128 的地址范围哦否则读出来的数据就不是你想要的数据了。 有读的函数当然就有写的函数了接下来我们介绍W25QXX_Write 这个函数该函数的作用与W25QXX_Flash_Read 的作用类似不过是用来写数据到W25Q128 里面的其代码如下 u8 W25QXX_BUFFER[4096]; void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) {u32 secpos;u16 secoff;u16 secremain;u16 i;u8 * W25QXX_BUF;W25QXX_BUFW25QXX_BUFFER;secposWriteAddr/4096;//扇区地址secoffWriteAddr%4096;//在扇区内的偏移secremain4096-secoff;//扇区剩余空间大小//printf(ad:%X,nb:%X\r\n,WriteAddr,NumByteToWrite);//测试用if(NumByteToWritesecremain)secremainNumByteToWrite;//不大于4096 个字节while(1){W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容for(i0;isecremain;i)//校验数据{if(W25QXX_BUF[secoffi]!0XFF)break;//需要擦除}if(isecremain)//需要擦除{W25QXX_Erase_Sector(secpos); //擦除这个扇区for(i0;isecremain;i) //复制{W25QXX_BUF[isecoff]pBuffer[i];}W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区}else W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写已经擦除了的,直接写入扇区剩余区间.if(NumByteToWritesecremain)break;//写入结束了else//写入未结束{secpos;//扇区地址增1secoff0;//偏移位置为0pBuffersecremain; //指针偏移WriteAddrsecremain; //写地址偏移NumByteToWrite-secremain; //字节数递减if(NumByteToWrite4096)secremain4096;//下一个扇区还是写不完else secremainNumByteToWrite; //下一个扇区可以写完了}}; }该函数可以在W25Q128 的任意地址开始写入任意长度必须不超过W25Q128 的容量的数据。我们这里简单介绍一下思路先获得首地址WriteAddr所在的扇区并计算在扇区内的偏移然后判断要写入的数据长度是否超过本扇区所剩下的长度如果不超过再先看看是否要擦除如果不要则直接写入数据即可如果要则读出整个扇区在偏移处开始写入指定长度的数据然后擦除这个扇区再一次性写入。当所需要写入的数据长度超过一个扇区的长度的时候我们先按照前面的步骤把扇区剩余部分写完再在新扇区内执行同样的操作如此循环直到写入结束。 接着打开w25qxx.h 文件可以看到这里面就定义了一些与W25Q128 操作相关的命令部分省略了这些命令在W25Q128 的数据手册时序上都有详细的介绍感兴趣的读者可以参考该数据手册。 #ifndef __FLASH_H #define __FLASH_H #include sys.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 0X5218extern u16 W25QXX_TYPE; //定义W25QXX芯片型号 #define W25QXX_CS PBout(12) //W25QXX的片选信号//指令表 #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 W25QXX_Init(void); u16 W25QXX_ReadID(void); //读取FLASH ID u8 W25QXX_ReadSR(void); //读取状态寄存器 void W25QXX_Write_SR(u8 sr); //写状态寄存器 void W25QXX_Write_Enable(void); //写使能 void W25QXX_Write_Disable(void); //写保护 void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite); void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead); //读取flash void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite);//写入flash void W25QXX_Erase_Chip(void); //整片擦除 void W25QXX_Erase_Sector(u32 Dst_Addr); //扇区擦除 void W25QXX_Wait_Busy(void); //等待空闲 void W25QXX_PowerDown(void); //进入掉电模式 void W25QXX_WAKEUP(void); //唤醒 #endif最后我们看看main.c 里面代码如下 #include led.h #include delay.h #include key.h #include sys.h #include lcd.h #include usart.h #include w25qxx.h //要写入到W25Q64的字符串数组 const u8 TEXT_Buffer[]{WarShipSTM32 SPI TEST}; #define SIZE sizeof(TEXT_Buffer)int main(void){ u8 key;u16 i0;u8 datatemp[SIZE];u32 FLASH_SIZE; u16 id 0;delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组22位抢占优先级2位响应优先级uart_init(115200); //串口初始化为115200LED_Init(); //初始化与LED连接的硬件接口LCD_Init(); //初始化LCD KEY_Init(); //按键初始化 W25QXX_Init(); //W25QXX初始化POINT_COLORRED;//设置字体为红色 LCD_ShowString(30,50,200,16,16,WarShip STM32); LCD_ShowString(30,70,200,16,16,SPI TEST); LCD_ShowString(30,90,200,16,16,ATOMALIENTEK);LCD_ShowString(30,110,200,16,16,2015/1/15); LCD_ShowString(30,130,200,16,16,KEY1:Write KEY0:Read); //显示提示信息 while(1){id W25QXX_ReadID();if (id W25Q128 || id NM25Q128)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;//DS0闪烁}LCD_ShowString(30,150,200,16,16,W25Q128 Ready!); FLASH_SIZE128*1024*1024; //FLASH 大小为16M字节POINT_COLORBLUE;//设置字体为蓝色 while(1){keyKEY_Scan(0);if(keyKEY1_PRES) //KEY1按下,写入W25QXX{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长度的数据LCD_ShowString(30,170,200,16,16,W25Q128 Write Finished!); //提示传送完成}if(keyKEY0_PRES) //KEY0按下,读取字符串并显示{LCD_ShowString(30,170,200,16,16,Start Read W25Q128.... );W25QXX_Read(datatemp,FLASH_SIZE-100,SIZE); //从倒数第100个地址处开始,读出SIZE个字节LCD_ShowString(30,170,200,16,16,The Data Readed Is: ); //提示传送完成LCD_ShowString(30,190,200,16,16,datatemp);//显示读到的字符串}i;delay_ms(10);if(i20){LED0!LED0;//提示系统正在运行 i0;} } }这部分代码和IIC 实验那部分代码大同小异我们就不多说了实现的功能就和IIC 差不多不过此次写入和读出的是SPI FLASH而不是EEPROM。 下载验证 在代码编译成功之后我们通过下载代码到ALIENTEK 战舰STM32 开发板上通过先按KEY1 按键写入数据然后按KEY0 读取数据得到如图29.4.1 所示 伴随DS0 的不停闪烁提示程序在运行。程序在开机的时候会检测W25Q128 是否存在如果不存在则会在TFTLCD 模块上显示错误信息同时DS0 慢闪。大家可以把PB14 和GND短接就可以看到报错了。 485通信实验 485接口原理 FLASH 操作 块bank/block两种叫法 扇区sector 页page 摘自flash 内部 扇区 页 块 区别 一般 一个块 bank有多个扇区 sector一个扇区sector有多个页page 块bank 扇区sector 页page 摘自FLASH ERASECHIP、BLOCK、SECTOR SPI FLASH 的ERASE、READ、WRITEERASE包括CHIP、BLOCK、SECTOR三方面的内容READ可对指定地址的单个及连续读取数据WRITE也有单个及连续写入数据。 ERASE的内容 CHIP(芯片)整片FLASH ERASE视容量大小占耗时间是最长的ERASE时间。BLOCK大部分的FLASH都以64K为单位空间ERASE具体可有针对地参考资料查证特别是FLASH的TOP或BOTTOM容易有差别。每个BLOCK的ERASE时间较快。SECTOR比BLOCK更小的单位ERASE时间最快。 这三方面的ERASE都带两个参数HIGH ADDRESS及LOW ADDRESS或可通过寄存器R0-R7来传递。使用时可根据利用到的FLASH空间及ERASE时间长短对程序的影响来作出适当的选择。 Flash相对于普通设备的特殊性 摘自https://www.cnblogs.com/sankye/articles/1638852.html Flash最小操作单位有些特殊。 一般设备比如硬盘/内存读取和写入都是以bit位为单位读取一个bit的值将某个值写入对应的地址的位都是可以按位操作的。 但是Flash由于物理特性使得内部存储的数据只能从1变成0这点可以从前面的内部实现机制了解到只是方便统一充电不方便单独的存储单元去放电所以才说只能从1变成0也就是释放电荷。 所以总结一下Flash的特殊性如下 项目普通设备(硬盘/内存等)Flash读取/写入的叫法读取/写入读取/编程(Program)①读取/写入的最小单位Bit/位Page/页擦除(Erase)操作的最小单位Bit/位Block/块②擦除操作的含义将数据删除/全部写入0将整个块都擦除成全是1也就是里面的数据都是0xFF③对于写操作直接写即可在写数据之前要先擦除然后再写 注 ① 之所以将写操作叫做编程是因为flash 和之前的EPROMEEPROM继承发展而来而之前的EEPROM(Electrically Erasable Programmable Read-Only Memory)往里面写入数据就叫做编程Program之所以这么称呼是因为其对数据的写入是需要用电去擦除/写入的就叫做编程。 ② 对于目前常见的页大小是2K/4K的Nand Flash其块的大小有128KB/256KB/512KB等。而对于Nor Flash常见的块大小有64K/32K等。 ③在写数据之前要先擦除内部就都变成0xFF了然后才能写入数据也就是将对应位由1变成0。 FLASH 模拟 EEPROM STM32 本身没有自带EEPROM但是STM32 具有IAP在应用编程功能所以我们可以把它的FLASH 当成EEPROM 来使用。本章我们将利用STM32 内部的FLASH 来实现第二十八章类似的效果不过这次我们是将数据直接存放在STM32 内部而不是存放在W25Q128。 STM32 FLASH 简介 不同型号的STM32其FLASH 容量也有所不同最小的只有16K 字节最大的达到了1024K 字节。战舰STM32 开发板选择的STM32F103ZET6 的FLASH 容量为512K 字节属于大容量产品另外还有中容量和小容量产品大容量产品的闪存模块组织如图39.1.1 所示 STM32 的闪存模块由主存储器、信息块和闪存存储器接口寄存器等3 部分组成。 主存储器该部分用来存放代码和数据常数(如const型常数)。对于大容量产品其被划分为256 页每页2K 字节。注意小容量和中容量产品则每页只有1K 字节。从上图可以看出主存储器的起始地址就是0X08000000B0、B1 都接GND 的时候就是从0X08000000开始运行代码的。 信息块该部分分为2 个小部分其中启动程序代码是用来存储ST自带的启动程序用于串口下载代码当B0 接V3.3B1 接GND 的时候运行的就是这部分代码。用户选择字节则一般用于配置写保护、读保护等功能本章不作介绍。 闪存存储器接口寄存器该部分用于控制闪存读写等是整个闪存模块的控制机构。 对主存储器和信息块的写入由内嵌的闪存编程/擦除控制器(FPEC)管理编程与擦除的高电压由内部产生。 在执行闪存写操作时任何对闪存的读操作都会锁住总线在写操作完成后读操作才能正确地进行既在进行写或擦除操作时不能进行代码或数据的读取操作。 FLASH操作(读、写、擦除) 闪存的读取 内置闪存模块可以在通用地址空间直接寻址任何32 位数据的读操作都能访问闪存模块的内容并得到相应的数据。读接口在闪存端包含一个读控制器还包含一个AHB 接口与CPU 衔接这个接口的主要工作是产生读闪存的控制信号并预取CPU 要求的指令块预取指令块仅用于在I-Code总线上的取指操作数据常量是通过D-Code总线访问的。这两条总线的访问目标是相同的闪存模块访问D-Code 将比预取指令优先级高。 这里要特别留意一个闪存等待时间因为CPU 运行速度比FLASH 快得多STM32F103的FLASH 最快访问速度≤24Mhz如果CPU 频率超过这个速度那么必须加入等待时间比如我们一般使用72Mhz的主频那么FLASH等待周期就必须设置为2该设置通过FLASH_ACR寄存器设置。 例如我们要从地址addr读取一个半字半字为16 为字为32 位可以通过如下的语句读取 data*(vu16*)addr;将addr 强制转换为vu16 指针然后取该指针所指向的地址的值即得到了addr 地址的值。 类似的将上面的vu16 改为vu8即可读取指定地址的一个字节。 闪存的写入 STM32 的闪存编程是由FPEC闪存编程和擦除控制器模块处理的这个模块包含7 个32 位寄存器他们分别是 ⚫ FPEC 键寄存器(FLASH_KEYR) ⚫ 选择字节键寄存器(FLASH_OPTKEYR) ⚫ 闪存控制寄存器(FLASH_CR) ⚫ 闪存状态寄存器(FLASH_SR) ⚫ 闪存地址寄存器(FLASH_AR) ⚫ 选择字节寄存器(FLASH_OBR) ⚫ 写保护寄存器(FLASH_WRPR) 其中FPEC 键寄存器总共有3 个键值 RDPRT 键0X000000A5 KEY10X45670123 KEY20XCDEF89AB STM32 复位后FPEC 模块是被保护的不能写入FLASH_CR 寄存器通过写入特定的序列到FLASH_KEYR 寄存器可以打开FPEC 模块即写入KEY1 和KEY2只有解除写保护后我们才能操作相关寄存器。 STM32 闪存的编程每次必须写入16 位不能单纯的写入8 位数据当FLASH_CR 寄存器的PG位为’1’时在一个闪存地址写入一个半字将启动一次编程写入任何非半字的数据FPEC 都会产生总线错误。 在编程过程中BSY位为’1’任何读写闪存的操作都会使CPU暂停直到此次闪存编程结束。 STM32 的FLASH 在编程的时候必须要求其写入地址的FLASH 是被擦除了的也就是其值必须是0XFFFF否则无法写入在FLASH_SR 寄存器的PGERR 位将得到一个警告。 STM23 的FLASH 编程过程如图39.1.2 所示 从上图可以得到闪存的编程顺序如下 ⚫ 检查FLASH_CR 的LOCK 是否解锁如果没有则先解锁 ⚫ 检查FLASH_SR 寄存器的BSY 位以确认没有其他正在进行的编程操作 ⚫ 设置FLASH_CR 寄存器的PG 位为’1’ ⚫ 在指定的地址写入要编程的半字 ⚫ 等待BSY 位变为’0’ ⚫ 读出写入的地址并验证数据 闪存的擦除 前面提到我们在STM32 的FLASH 编程的时候要先判断缩写地址是否被擦除了STM32 的闪存擦除分为两种页擦除和整片擦除。页擦除过程如图39.1.3 所示 从上图可以看出STM32 的页擦除顺序为 ⚫ 检查FLASH_CR 的LOCK 是否解锁如果没有则先解锁 ⚫ 检查FLASH_SR 寄存器的BSY 位以确认没有其他正在进行的闪存操作 ⚫ 设置FLASH_CR 寄存器的PER 位为’1’ ⚫ 用FLASH_AR 寄存器选择要擦除的页 ⚫ 设置FLASH_CR 寄存器的STRT 位为’1’ ⚫ 等待BSY 位变为’0’(擦除完毕) ⚫ 读出被擦除的页并做验证 本章我们只用到了STM32 的页擦除功能。 寄存器 FPEC 键寄存器FLASH_KEYR。该寄存器各位描述如图39.1.4 所示 该寄存器主要用来解锁FPEC必须在该寄存器写入特定的序列KEY1 和KEY2解锁后才能对FLASH_CR 寄存器进行写操作。 闪存控制寄存器FLASH_CR。该寄存器的各位描述如图39.1.5 所示 该寄存器我们本章只用到了它的LOCK、STRT、PER 和PG 等4 个位。 LOCK 位该位用于指示FLASH_CR 寄存器是否被锁住该位在检测到正确的解锁序列后硬件将其清零。在一次不成功的解锁操作后在下次系统复位之前该位将不再改变。 STRT 位该位用于开始一次擦除操作。在该位写入1 将执行一次擦除操作。 PER 位该位用于选择页擦除操作在页擦除的时候需要将该位置1。 PG 位该位用于选择编程操作在往FLASH 写数据的时候该位需要置1。 FLASH_CR 的其他位我们就不在这里介绍了请大家参考《STM32F10xxx 闪存编程参考手册》第18 页。 闪存状态寄存器FLASH_SR。该寄存器各位描述如图39.1.6 所示 该寄存器主要用来指示当前FPEC 的操作编程状态。 闪存地址寄存器FLASH_AR。该寄存器各位描述如图39.1.7 所示 该寄存器在本章我们主要用来设置要擦除的页。 关于STM32 FLASH 的基础知识介绍我们就介绍到这。更详细的介绍请参考《STM32F10xxx 闪存编程参考手册》。 库函数(本质操作寄存器) 在stm32f10x_flash.c 以及stm32f10x_flash.h 文件中。 1、锁定 / 解锁函数 上面讲解到在对FLASH 进行写操作前必须先解锁解锁操作也就是必须在FLASH_KEYR 寄存器写入特定的序列KEY1 和KEY2,固件库函数实现很简单 void FLASH_Unlock(void)同样的道理在对FLASH 写操作完成之后我们要锁定FLASH使用的库函数是 void FLASH_Lock(void)2、写操作函数 固件库提供了三个FLASH 写函数 FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data); FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data); FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data);FLASH_ProgramWord 为32 位字写入函数其他分别为16 位半字写入和用户选择字节写入函数。 这里需要说明32 位字节写入实际上是写入的两次16 位数据写完第一次后地址2这与我们前面讲解的STM32 闪存的编程每次必须写入16 位并不矛盾。写入8位实际也是占用的两个地址了跟写入16 位基本上没啥区别。 3、擦除函数 固件库提供三个FLASH 擦除函数 FLASH_Status FLASH_ErasePage(uint32_t Page_Address); FLASH_Status FLASH_EraseAllPages(void); FLASH_Status FLASH_EraseOptionBytes(void);第一个函数是页擦除函数根据页地址擦除特定的页数据 第二个函数是擦除所有的页数据 第三个函数是擦除用户选择字节数据。这三个函数使用非常简单。 4、获取FLASH 状态 主要是用的函数是 FLASH_Status FLASH_GetStatus(void)返回值是通过枚举类型定义的 typedef enum {FLASH_BUSY 1,//忙FLASH_ERROR_PG,//编程错误FLASH_ERROR_WRP,//写保护错误FLASH_COMPLETE,//操作完成FLASH_TIMEOUT//操作超时 }FLASH_Status;从这里面我们可以看到FLASH 操作的5 个状态每个代表的意思我们在后面注释了。 5、等待操作完成函数 在执行闪存写操作时任何对闪存的读操作都会锁住总线。 所以在每次操作之前我们都要等待上一次操作完成这次操作才能开始。使用的函数是 FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout)入口参数为等待时间返回值是FLASH 的状态这个很容易理解这个函数本身我们在固件库中使用得不多但是在固件库函数体中间可以多次看到。 6、读FLASH 特定地址数据函数 读取FLASH 指定地址的半字的函数固件库并没有给出来这里我们自己写的一个函数 u16 STMFLASH_ReadHalfWord(u32 faddr) {return *(vu16*)faddr; }硬件设计 本章实验功能简介开机的时候先显示一些提示信息然后在主循环里面检测两个按键按键WK_UP执行FLASH写操作按键KEY1执行FLASH读操作在TFTLCD 模块上显示相关信息。同时用DS0 提示程序正在运行。 所要用到的硬件资源如下 指示灯DS0WK_UP 和KEY1 按键TFTLCD 模块STM32 内部FLASH 本章需要用到的资源和电路连接在之前已经全部有介绍过了。 软件设计 打开FLASH 模拟EEPROM 实验工程我们添加了两个文件stmflash.c 和stm32flash.h 。 同时引入了固件库flash 操作文件stm32f10x_flash.c 和头文件stm32f10x_flash.h。 这个是前面EEPROM写入思路 这个是FLASH写入思路 打开stmflash.c 文件代码如下 stmflash.h #ifndef __STMFLASH_H__ #define __STMFLASH_H__ #include sys.h //用户根据自己的需要设置 #define STM32_FLASH_SIZE 512 //所选STM32的FLASH容量大小(单位为K) #define STM32_FLASH_WREN 1 //使能FLASH写入 0-不使能 1-使能//FLASH起始地址 #define STM32_FLASH_BASE 0x08000000 //STM32 FLASH的起始地址 //FLASH解锁键值u16 STMFLASH_ReadHalfWord(u32 faddr); //读出半字 void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite); //指定地址开始写入指定长度的数据 void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead); //指定地址开始读出指定长度的数据//测试写入 void Test_Write(u32 WriteAddr,u16 WriteData); #endifstmflash.c #include stmflash.h #include delay.h #include usart.h//读取指定地址的半字(16位数据) //faddr:读地址(此地址必须为2的倍数!!) //返回值:对应数据. u16 STMFLASH_ReadHalfWord(u32 faddr) {return *(vu16*)faddr; } #if STM32_FLASH_WREN //如果使能了写 //不检查的写入(写之前要擦除检查是否擦除了这里是已经检查过了直接写入) //WriteAddr:起始地址 //pBuffer:数据指针 //NumToWrite:半字(16位)数 void STMFLASH_Write_NoCheck(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite) { u16 i;for(i0;iNumToWrite;i){FLASH_ProgramHalfWord(WriteAddr,pBuffer[i]);WriteAddr2;//地址增加2.} } //从指定地址开始写入指定长度的数据 //WriteAddr:起始地址(此地址必须为2的倍数!!) //pBuffer:数据指针 //NumToWrite:半字(16位)数(就是要写入的16位(半字)数据的个数.) #if STM32_FLASH_SIZE256 #define STM_SECTOR_SIZE 1024 //页大小 #else #define STM_SECTOR_SIZE 2048 #endif u16 STMFLASH_BUF[STM_SECTOR_SIZE/2];//最多是2K字节 void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite) {u32 secpos; //扇区地址u16 secoff; //扇区内偏移地址(16位字计算)u16 secremain; //扇区内剩余地址(16位字计算) u16 i; u32 offaddr; if(WriteAddrSTM32_FLASH_BASE||(WriteAddr(STM32_FLASH_BASE1024*STM32_FLASH_SIZE)))return;//非法地址地址越界FLASH_Unlock(); //解锁offaddrWriteAddr-STM32_FLASH_BASE; //实际偏移地址.secposoffaddr/STM_SECTOR_SIZE; //落在第几页sector 0~127 for STM32F103RBT6secoff(offaddr%STM_SECTOR_SIZE)/2; //在扇区内的偏移(2个字节为基本单位.)secremainSTM_SECTOR_SIZE/2-secoff; //扇区剩余空间大小 if(NumToWritesecremain)secremainNumToWrite;//不大于该扇区范围while(1) { //读到buf数组里面 STMFLASH_Read(secpos*STM_SECTOR_SIZESTM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容for(i0;isecremain;i)//是否擦除{if(STMFLASH_BUF[secoffi]!0XFFFF)break;//需要擦除}if(isecremain)//擦除操作{FLASH_ErasePage(secpos*STM_SECTOR_SIZESTM32_FLASH_BASE);//擦除这个扇区for(i0;isecremain;i)//复制到flash里面{STMFLASH_BUF[isecoff]pBuffer[i]; }STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZESTM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区 }else STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//不用擦除,直接写入到扇区剩余区间 if(NumToWritesecremain)break;//写入结束了 就是没跨越页else//写入未结束 跨页了{secpos; //扇区地址增1secoff0; //偏移位置为0 pBuffersecremain; //指针偏移WriteAddr(secremain*2);//写地址偏移 NumToWrite-secremain; //字节(16位)数递减if(NumToWrite(STM_SECTOR_SIZE/2))secremainSTM_SECTOR_SIZE/2;//下一个扇区还是写不完else secremainNumToWrite;//下一个扇区可以写完了} }; FLASH_Lock();//上锁 } #endif//从指定地址开始读出指定长度的数据 //ReadAddr:起始地址 //pBuffer:数据指针 //NumToWrite:半字(16位)数 void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead) {u16 i;for(i0;iNumToRead;i){pBuffer[i]STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节(半字)ReadAddr2;//偏移2个字节. } }//WriteAddr:起始地址 //WriteData:要写入的数据 void Test_Write(u32 WriteAddr,u16 WriteData) {STMFLASH_Write(WriteAddr,WriteData,1);//写入一个字 }STMFLASH_ReadHalfWord()函数的实现原理我们在前面已经详细讲解了。 STMFLASH_Write()函数用于在STM32 的指定地址写入指定长度的数据该函数的实现基本类似第29 章的W25QXX_Write 函数不过对写入地址是有要求的必须保证以下两点 1该地址必须是用户代码区以外的地址。 2该地址必须是2 的倍数STM32 FLASH 的要求每次必须写入16 位。 该函数的STMFLASH_BUF 数组也是根据所用STM32 的FLASH 容量来确定的战舰STM32 开发板的FLASH 是512K 字节所以STM_SECTOR_SIZE 的值为512故该数组大小为2K 字节。 main.c #include led.h #include delay.h #include key.h #include sys.h #include lcd.h #include usart.h #include stmflash.h//要写入到STM32 FLASH的字符串数组 const u8 TEXT_Buffer[]{STM32F103 FLASH TEST}; #define SIZE sizeof(TEXT_Buffer) //数组长度 #define FLASH_SAVE_ADDR 0X08070000 //设置FLASH 保存地址(必须为偶数且其值要大于本代码所占用FLASH的大小0X08000000)int main(void){ u8 key;u16 i0;u8 datatemp[SIZE];delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组22位抢占优先级2位响应优先级uart_init(115200); //串口初始化为115200LED_Init(); //初始化与LED连接的硬件接口KEY_Init(); //初始化按键LCD_Init(); //初始化LCD POINT_COLORRED; //设置字体为红色 LCD_ShowString(30,50,200,16,16,WarShip STM32); LCD_ShowString(30,70,200,16,16,FLASH EEPROM TEST); LCD_ShowString(30,90,200,16,16,ATOMALIENTEK);LCD_ShowString(30,110,200,16,16,2015/1/18); LCD_ShowString(30,130,200,16,16,KEY1:Write KEY0:Read);while(1){keyKEY_Scan(0);if(keyKEY1_PRES) //KEY1按下,写入STM32 FLASH{LCD_Fill(0,170,239,319,WHITE);//清除半屏 LCD_ShowString(30,170,200,16,16,Start Write FLASH....);STMFLASH_Write(FLASH_SAVE_ADDR,(u16*)TEXT_Buffer,SIZE);LCD_ShowString(30,170,200,16,16,FLASH Write Finished!);//提示传送完成}if(keyKEY0_PRES) //KEY0按下,读取字符串并显示{LCD_ShowString(30,170,200,16,16,Start Read FLASH.... );STMFLASH_Read(FLASH_SAVE_ADDR,(u16*)datatemp,SIZE);LCD_ShowString(30,170,200,16,16,The Data Readed Is: );//提示传送完成LCD_ShowString(30,190,200,16,16,datatemp);//显示读到的字符串}i;delay_ms(10); if(i20){LED0!LED0;//提示系统正在运行 i0;} } }在代码编译成功之后我们通过下载代码到ALIENTEK 战舰STM32 开发板上通过先按WK_UP 按键写入数据然后按KEY1 读取数据DS0不停闪烁提示程序在运行。如图39.4.1 所示 串口IAP实验(在程序中编程In Application Programming) IAP配置 bootloader的三个作用(程序要实现的功能) 1、接收APP的bin文件 2、将这个文件写到flash的某个区域 3、实现跳转 IAP即在应用编程。很多单片机都支持这个功能STM32 也不例外。在之前的FLASH模拟EEPROM 实验里面我们学习了STM32 的FLASH 自编程本章我们将结合FLASH 自编程的知识通过STM32 的串口实现一个简单的IAP 功能 IAP 简介 IAPIn Application Programming即在应用编程IAP 是用户自己的程序在运行过程中对User Flash 的部分区域进行烧写目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。 通常实现IAP 功能时即用户程序运行中作自身的更新操作需要在设计固件程序时编写两个项目代码第一个项目程序通过某种通信方式(如USB、USART)接收程序或数据执行对第二部分代码的更新第二个项目代码是真正的功能代码。这两部分项目代码都同时烧录在User Flash 中当芯片上电后首先是第一个项目代码开始运行它作如下操作 1检查是否需要对第二部分代码进行更新 2如果不需要更新则转到4 3执行更新操作 4跳转到第二部分代码执行 第一部分代码必须通过其它手段如JTAG或ISP烧入第二部分代码可以使用第一部分代码IAP功能烧入也可以和第一部分代码一起烧入以后需要程序更新时再通过第一部分IAP代码更新。 我们将第一个项目代码称之为Bootloader程序第二个项目代码称之为APP程序他们存放在STM32 FLASH 的不同地址范围一般从最低地址区开始存放Bootloader紧跟其后的就是APP 程序注意如果FLASH 容量足够是可以设计很多APP 程序的本章我们只讨论一个APP 程序的情况。这样我们就是要实现2 个程序Bootloader 和APP。 STM32 的APP 程序不仅可以放到FLASH 里面运行也可以放到SRAM 里面运行本章我们将制作两个APP一个用于FLASH 运行一个用于SRAM 运行。 IAP运作流程 我们先来看看STM32 正常的程序运行流程如图52.1.1 所示 STM32 的内部闪存FLASH地址起始于0x08000000一般情况下程序文件就从此地址开始写入。此外STM32 是基于Cortex-M3 内核的微控制器其内部通过一张“中断向量表”来响应中断程序启动后将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动而这张“中断向量表”的起始地址是0x08000004当中断来临STM32 的内部硬件机制亦会自动将PC 指针定位到“中断向量表”处并根据中断源取出对应的中断向量执行中断服务程序。 在图52.1.1 中STM32 在复位后先从0X08000004 地址取出复位中断向量的地址并跳转到复位中断服务程序如图标号①所示在复位中断服务程序执行完之后会跳转到我们的main 函数如图标号②所示而我们的main 函数一般都是一个死循环在main 函数执行过程中如果收到中断请求发生重中断此时STM32 强制将PC 指针指回中断向量表处如图标号③所示然后根据中断源进入相应的中断服务程序如图标号④所示在执行完中断服务程序以后程序再次返回main 函数执行如图标号⑤所示。 当加入IAP 程序之后程序运行流程如图52.1.2 所示 在图52.1.2 所示流程中STM32 复位后还是从0X08000004 地址取出复位中断向量的地址并跳转到复位中断服务程序在运行完复位中断服务程序之后跳转到IAP 的main 函数如图标号①所示此部分同图52.1.1 一样在执行完IAP 以后即将新的APP 代码写入STM32的FLASH灰底部分。新程序的复位中断向量起始地址为0X08000004NM跳转至新写入程序的复位向量表取出新程序的复位中断向量的地址并跳转执行新程序的复位中断服务程序随后跳转至新程序的main 函数如图标号②和③所示同样main 函数为一个死循环并且注意到此时STM32 的FLASH在不同位置上共有两个中断向量表。 在main 函数执行过程中如果CPU 得到一个中断请求PC 指针仍强制跳转到地址0X08000004 中断向量表处而不是新程序的中断向量表如图标号④所示程序再根据我们设置的中断向量表偏移量跳转到新的中断服务程序中如图标号⑤所示在执行完中断服务程序后程序返回main 函数继续运行如图标号⑥所示。 通过以上两个过程的分析我们知道IAP 程序必须满足两个要求 1新程序必须在IAP 程序之后的某个偏移量为x 的地址开始 2必须设置新程序的中断向量表偏移移动的偏移量为x 本章我们有2 个APP 程序一个为FLASH 的APP程序在FLASH 中运行另外一个位SRAM 的APP程序运行在SRAM 中图52.1.2 虽然是针对FLASH APP 来说的但是在SRAM 里面运行的过程和FLASH 基本一致只是需要设置向量表的地址为SRAM 的地址。 APP 程序起始地址设置方法 随便打开一个之前的实例工程点击Options for Target→Target 选项卡如图52.1.3 所示 默认的条件下图中IROM1 的起始地址(Start)一般为0X08000000大小(Size)为0X80000即从0X08000000 开始的512K 空间为我们的程序存储,因为我们的STM32F103ZET6 的FLASH大小是512K。 而图中我们设置起始地址(Start)为0X08010000即偏移量为0X10000(64K字节)因而留给APP 用的FLASH 空间(Size)只有0X80000-0X100000X70000(448K 字节)大小了。设置好Start 和Szie就完成APP 程序的起始地址设置。 这里的64K字节需要大家根据Bootloader 程序大小进行选择比如我们本章的Bootloader程序为22K 左右理论上我们只需要确保APP 起始地址在Bootloader 之后并且偏移量为0X200的倍数即可(相关知识请参考http://www.openedv.com/posts/list/392.htm)。这里我们选择64K(0X10000)字节留了一些余量方便Bootloader 以后的升级修改。 这是针对FLASH APP 的起始地址设置如果是SRAM APP那么起始地址设置如图52.1.4所示 (上图中IROM1其实就是RAM)从地址0X20000000偏移0X1000(0X1000是4K 给Bootloader 程序使用)开始存放APP 代码大小为0XC000(48K 字节)。因为整个STM32F103ZET6 的SRAM大小为64K 字节所以IRAM1 (SRAM)的起始地址变为0X2000D000(0x200010000xC0000X2000D000 )大小只有0X3000 (12K 字节)。 这样整个STM32F103ZET6 的SRAM 分配情况为最开始的4K 给Bootloader程序使用随后的48K存放APP程序最后12K用作APP 程序的内存。这个分配关系大家可以根据自己的实际情况修改需要保证偏移量为0X200 的倍数(我们这里为0X1000)。 中断向量表的偏移量设置方法 之前我们讲解过在系统启动的时候会首先调用systemInit 函数初始化时钟系统同时systemInit 还完成了中断向量表的设置我们可以打开systemInit 函数看看函数体的结尾处有这样几行代码 #ifdef VECT_TAB_SRAM SCB-VTOR SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */ #else SCB-VTOR FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */ #endifVTOR 寄存器存放的是中断向量表的起始地址。默认的情况VECT_TAB_SRAM 没有定义所以执行SCB-VTOR FLASH_BASE | VECT_TAB_OFFSET。 对于FLASH APP设置为FLASH_BASE偏移量0x10000所以在FLASH APP 的main 函数最开头处添加如下代码实现中断向量表的起始地址的重设 SCB-VTOR FLASH_BASE | 0x10000;当使用SRAM APP 的时候我们设置起始地址为SRAM_bASE0x1000同样的方法在SRAM APP 的main 函数最开始处添加下面代码 SCB-VTOR SRAM_BASE | 0x1000;这样我们就完成了中断向量表偏移量的设置。 hex转bin文件 MDK 默认生成的文件是.hex 文件我们希望生成的文件是.bin 文件(BIN文件和HEX文件区别)。这里我们通过MDK 自带的格式转换工具fromelf.exe来实现.axf 文件到.bin 文件的转换。该工具在MDK 的安装目录\ARM\BIN40 文件夹里面。 fromelf.exe 转换工具的语法格式为fromelf [options] input_file。其中options 有很多选项可以设置详细使用请参考光盘《mdk 如何生成bin 文件.doc》. 本章我们通过在MDK 点击Options for Target→User 选项卡在After Build/Rebuild 栏勾选Run #1并写入D:\tools\mdk5.14\ARM\ARMCC\bin\fromelf.exe --bin -o …\OBJ\RTC.bin …\OBJ\RTC.axf。如图52.1.6 所示 通过这一步设置我们就可以在MDK 编译成功之后调用fromelf.exe注意我的MDK 是安装在D:\tools\mdk5.14 文件夹下如果你是安装在其他目录请根据你自己的目录修改fromelf.exe 的路径根据当前工程的RTC.axf如果是其他的名字请记住修改这个文件存放在OBJ 目录下面格式为xxx.axf生成一个RTC.bin 的文件。并存放在axf 文件相同的目 录下即工程的OBJ 文件夹里面。在得到.bin 文件之后我们只需要将这个bin 文件传送给单片机即可执行IAP 升级。 总结 APP 程序的生成步骤 1设置APP程序的起始地址和存储空间大小 对于在FLASH里面运行的APP程序我们可以按照图52.1.3的设置。对于SRAM里面运行的APP程序我们可以参考图52.1.4的设置。 2设置中断向量表偏移量 这一步按照上面讲解重新设置SCB-VTOR 的值即可。 3设置编译后运行fromelf.exe生成.bin文件. 通过在User 选项卡设置编译后调用fromelf.exe根据.axf文件生成.bin文件用于IAP更新。 以上3 个步骤我们就可以得到一个.bin的APP程序通过Bootlader程序即可实现更新。 硬件设计 本章实验Bootloader 部分功能简介开机的时候先显示提示信息然后等待串口输入接收APP 程序无校验一次性接收在串口接收到APP 程序之后即可执行IAP指示灯DS0用于指示程序运行状态。 如果是SRAM APP通过按下KEY0 即可执行这个收到的SRAM APP 程序。如果是FLASH APP则需要先按下WK_UP按键将串口接收到的APP程序存放到STM32 的FLASH之后再按KEY1执行FLASH APP程序。KEY2按键可以手动清除串口接收到的APP程序(实际代码只是显示清除)。 本实验用到的资源如下 1指示灯DS0 2四个按键KEY0/KEY1/KEY2/WK_UP 3串口 4TFTLCD 模块 软件设计 本章我们总共需要3 个程序1、Bootloader2、FLASH APP3、SRAM APP其中我们选择之前做过的RTC 实验在第二十章介绍来做为FLASH APP 程序起始地址为0X08010000选择触摸屏实验在第三十二章介绍来做SRAM APP 程序起始地址为0X20001000。Bootloader 则是通过TFTLCD 显示实验在第十八章介绍修改得来。 iap.c #include sys.h #include delay.h #include usart.h #include stmflash.h//要对flash进行操作引入头文件 #include iap.hiapfun jump2app; u16 iapbuf[1024]; //appxaddr:应用程序的起始地址 //appbuf:应用程序CODE. //appsize:应用程序大小(字节). void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 appsize) {u16 t;u16 i0;u16 temp;u32 fwaddrappxaddr;//当前写入的地址u8 *dfuappbuf;for(t0;tappsize;t2){ temp(u16)dfu[1]8;temp(u16)dfu[0]; dfu2;//偏移2个字节iapbuf[i]temp; if(i1024){i0;STMFLASH_Write(fwaddr,iapbuf,1024); fwaddr2048;//偏移2048 162*8.所以要乘以2.}}if(i)STMFLASH_Write(fwaddr,iapbuf,i);//将最后的一些内容字节写进去. }//跳转到应用程序段 //appxaddr:用户代码起始地址. void iap_load_app(u32 appxaddr) {if(((*(vu32*)appxaddr)0x2FFE0000)0x20000000) //检查栈顶地址是否合法.{ jump2app(iapfun)*(vu32*)(appxaddr4); //用户代码区第二个字为程序开始地址(复位地址) MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)jump2app(); //跳转到APP.} } iap_write_appbin 函数将存放在串口接收buf 里面的APP 程序写入到FLASH。iap_load_app 函数跳转到APP 程序运行其参数appxaddr为APP 程序的起始地址。 程序先判断栈顶地址是否合法在得到合法的栈顶地址后通过MSR_MSP 函数该函数在sys.c 文件设置栈顶地址最后通过一个虚拟的函数jump2app跳转到APP 程序执行代码实现IAP→APP 的跳转。 打开iap.h 代码如下 iap.h #ifndef __IAP_H__ #define __IAP_H__ #include sys.h typedef void (*iapfun)(void); //定义一个函数类型的参数.#define FLASH_APP1_ADDR 0x08010000 //第一个应用程序起始地址(存放在FLASH)//保留0X08000000~0X0800FFFF的空间(64K 根据实际大小来)为IAP使用void iap_load_app(u32 appxaddr); //执行flash里面的app程序起始地址为appxaddr void iap_load_appsram(u32 appxaddr); //执行SRAM里面的app程序 void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 applen); //在flash指定地址appxaddr开始,写入bin #endif我们是通过串口接收APP 程序的我们将usart.c 和usart.h做了稍微修改在usart.h 中我们定义USART_REC_LEN 为55K 字节也就是串口最大一次可以接收55K 字节的数据这也是本Bootloader 程序所能接收的最大APP 程序大小。然后新增一个USART_RX_CNT 的变量用于记录接收到的文件大小而USART_RX_STA 不再使用。 usart.h #ifndef __USART_H #define __USART_H #include stdio.h #include sys.h #define USART_REC_LEN 55*1024 //定义最大接收字节数 55K字节bin文件不能大于55k 实际应用中根据工程需要修改 #define EN_USART1_RX 1 //使能1/禁止0串口1接收extern u8 USART_RX_BUF[USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.末字节为换行符 extern u16 USART_RX_STA; //接收状态标记 extern u16 USART_RX_CNT; //接收的字节数 //如果想串口中断接收请不要注释以下宏定义 void uart_init(u32 bound); #endif打开usart.c可以看到我们修改USART1_IRQHandler 部分代码如下 usart.c #include sys.h #include usart.h // //如果使用ucos,则包括下面的头文件即可. #if SYSTEM_SUPPORT_OS #include includes.h //ucos 使用 #endif// //加入以下代码,支持printf函数,而不需要选择use MicroLIB #if 1 #pragma import(__use_no_semihosting) //标准库需要的支持函数 struct __FILE { int handle; }; FILE __stdout; //定义_sys_exit()以避免使用半主机模式 void _sys_exit(int x) { x x; } //重定义fputc函数 int fputc(int ch, FILE *f) { while((USART1-SR0X40)0);//循环发送,直到发送完毕 USART1-DR (u8) ch; return ch; } #endif /*使用microLib的方法*//* int fputc(int ch, FILE *f) {USART_SendData(USART1, (uint8_t) ch);while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET) {} return ch; } int GetKey (void) { while (!(USART1-SR USART_FLAG_RXNE));return ((int)(USART1-DR 0x1FF)); } */#if EN_USART1_RX //如果使能了接收 //串口1中断服务程序 //注意,读取USARTx-SR能避免莫名其妙的错误 u8 USART_RX_BUF[USART_REC_LEN] __attribute__ ((at(0X20001000)));//接收缓冲,最大USART_REC_LEN个字节,并限定数组存放在SRAM区域的起始地址为0X20001000(前面留出一定空间给bootloader使用) //接收状态 //bit15 接收完成标志 //bit14 接收到0x0d //bit13~0 接收到的有效字节数目 u16 USART_RX_STA0; //接收状态标记 u16 USART_RX_CNT0; //接收的字节数 void uart_init(u32 bound) {//GPIO端口设置GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能USART1GPIOA时钟//USART1_TX GPIOA.9GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; //PA.9GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; //复用推挽输出GPIO_Init(GPIOA, GPIO_InitStructure);//初始化GPIOA.9//USART1_RX GPIOA.10初始化GPIO_InitStructure.GPIO_Pin GPIO_Pin_10;//PA10GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING;//浮空输入GPIO_Init(GPIOA, GPIO_InitStructure);//初始化GPIOA.10 //Usart1 NVIC 配置NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority3 ;//抢占优先级3NVIC_InitStructure.NVIC_IRQChannelSubPriority 3; //子优先级3NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; //IRQ通道使能NVIC_Init(NVIC_InitStructure); //根据指定的参数初始化VIC寄存器//USART 初始化设置USART_InitStructure.USART_BaudRate bound;//串口波特率USART_InitStructure.USART_WordLength USART_WordLength_8b;//字长为8位数据格式USART_InitStructure.USART_StopBits USART_StopBits_1;//一个停止位USART_InitStructure.USART_Parity USART_Parity_No;//无奇偶校验位USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None;//无硬件数据流控制USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; //收发模式USART_Init(USART1, USART_InitStructure); //初始化串口1USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断USART_Cmd(USART1, ENABLE); //使能串口1 }void USART1_IRQHandler(void)//每接收一个字节都要执行中断服务函数 {u8 res; #ifdef OS_CRITICAL_METHOD //如果OS_CRITICAL_METHOD定义了,说明使用ucosII了.OSIntEnter(); #endifif(USART1-SR(15))//接收到数据{ resUSART1-DR; if(USART_RX_CNTUSART_REC_LEN){USART_RX_BUF[USART_RX_CNT]res;//接收到的数据存放在buf里后续写入指定flash区域USART_RX_CNT; }} #ifdef OS_CRITICAL_METHOD //如果OS_CRITICAL_METHOD定义了,说明使用ucosII了.OSIntExit(); #endif } #endif 这里我们指定USART_RX_BUF 的地址是从0X20001000 开始该地址也就是SRAM APP程序的起始地址。然后在USART1_IRQHandler 函数里面将串口发送过来的数据全部接收到USART_RX_BUF并通过USART_RX_CNT 计数。 main.c #include led.h #include delay.h #include key.h #include sys.h #include lcd.h #include usart.h #include stmflash.h #include iap.hint main(void) { u8 t;u8 key;u16 oldcount0; //老的串口接收数据值u16 applenth0; //接收到的app代码长度u8 clearflag0; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级2位响应优先级uart_init(115200); //串口初始化为115200delay_init(); //延时初始化 LED_Init(); //初始化与LED连接的硬件接口KEY_Init(); //初始化按键LCD_Init(); //初始化LCD POINT_COLORRED;//设置字体为红色 LCD_ShowString(30,50,200,16,16,Warship STM32); LCD_ShowString(30,70,200,16,16,IAP TEST); LCD_ShowString(30,90,200,16,16,ATOMALIENTEK);LCD_ShowString(30,110,200,16,16,2015/1/27); LCD_ShowString(30,130,200,16,16,KEY_UP:Copy APP2FLASH);LCD_ShowString(30,150,200,16,16,KEY2:Erase SRAM APP);LCD_ShowString(30,170,200,16,16,KEY1:Run FLASH APP);LCD_ShowString(30,190,200,16,16,KEY0:Run SRAM APP);POINT_COLORBLUE;//显示提示信息POINT_COLORBLUE;//设置字体为蓝色 while(1){if(USART_RX_CNT){if(oldcountUSART_RX_CNT)//新周期内,没有收到任何数据,认为本次数据接收完成.{applenthUSART_RX_CNT;oldcount0;USART_RX_CNT0;printf(用户程序接收完成!\r\n);//程序接收完成printf(代码长度:%dBytes\r\n,applenth);}else oldcountUSART_RX_CNT; }t;delay_ms(10);if(t30){LED0!LED0;t0;if(clearflag){clearflag--;if(clearflag0)LCD_Fill(30,210,240,21016,WHITE);//清除显示}} keyKEY_Scan(0); //按键扫描if(keyWKUP_PRES)//按键按下 APP程序存放到FLASH{if(applenth){printf(开始更新固件...\r\n); LCD_ShowString(30,210,200,16,16,Copying APP2FLASH...);if(((*(vu32*)(0X200010004))0xFF000000)0x08000000)//判断是否为0X08XXXXXX.{ iap_write_appbin(FLASH_APP1_ADDR,USART_RX_BUF,applenth);//将bin程序写入到FLASH LCD_ShowString(30,210,200,16,16,Copy APP Successed!!);printf(固件更新完成!\r\n); }else {LCD_ShowString(30,210,200,16,16,Illegal FLASH APP! ); printf(非FLASH应用程序!\r\n);}}else {printf(没有可以更新的固件!\r\n);LCD_ShowString(30,210,200,16,16,No APP!);}clearflag7;//标志更新了显示,并且设置7*300ms后清除显示 }if(keyKEY2_PRES)//按键2按下{if(applenth){ printf(固件清除完成!\r\n); LCD_ShowString(30,210,200,16,16,APP Erase Successed!);applenth0;//置0清除固件}else {printf(没有可以清除的固件!\r\n);LCD_ShowString(30,210,200,16,16,No APP!);}clearflag7;//标志更新了显示,并且设置7*300ms后清除显示 }if(keyKEY1_PRES)//按键1按下 跳转执行FLASH APP代码{printf(开始执行FLASH用户代码!!\r\n);if(((*(vu32*)(FLASH_APP1_ADDR4))0xFF000000)0x08000000)//判断是否为0X08XXXXXX.{ iap_load_app(FLASH_APP1_ADDR);//跳转执行FLASH APP代码}else {printf(非FLASH应用程序,无法执行!\r\n);LCD_ShowString(30,210,200,16,16,Illegal FLASH APP!); } clearflag7;//标志更新了显示,并且设置7*300ms后清除显示 }if(keyKEY0_PRES)//按键0按下 跳转执行SRAM地址程序{printf(开始执行SRAM用户代码!!\r\n);if(((*(vu32*)(0X200010004))0xFF000000)0x20000000)//判断是否为0X20XXXXXX.{ iap_load_app(0X20001000);//执行SRAM地址程序}else {printf(非SRAM应用程序,无法执行!\r\n);LCD_ShowString(30,210,200,16,16,Illegal SRAM APP!); } clearflag7;//标志更新了显示,并且设置7*300ms后清除显示 } } }该段代码实现了串口数据处理以及IAP 更新和跳转等各项操作。 Bootloader 程序就设计完成了一般要求bootloader 程序越小越好给APP 省空间我们把一些不需要用到的.c 文件全部去掉最后得到工程截图如图52.3.1 所示 从上图结果可以看出虽然去掉了一些不用的.c 文件但是Bootloader 大小还是有36K 左右比较大主要原因是液晶驱动和printf 占用了比较多的flash如果大家想进一步删减可以去掉LCD 显示和printf 等不过我们在本章为了演示效果所以保留了这些代码。 FLASH APP 和SRAM APP 两部分代码我们在实验目录下提供了两个实验供大家参考不过要提醒大家根据我们的设置FLASH APP 的起始地址必须是0X08010000而SRAM APP的起始地址必须是0X20001000。 下载验证 在代码编译成功之后我们下载代码到ALIENTEK 战舰STM32 开发板上得到如图52.4.1所示 此时我们可以通过串口发送FLASH APP 或者SRAM APP 到战舰STM32 开发板如图52.4.2 所示 首先找到开发板USB 转串口的串口号打开串口我电脑是COM3然后设置波特率为115200图中标号1 所示然后点击打开文件按钮如图标号2 所示找到APP 程序生成的.bin 文件注意文件类型得选择所有文件默认是只打开txt 文件的最后点击发送文件图中标号3 所示将.bin 文件发送给战舰STM32F103发送完成后XCOM 会提示文件发 送完毕。 开发板在收到APP 程序之后我们就可以通过KEY0/KEY1 运行这个APP 程序了如果是FLASH APP则先需要通过WK_UP 将其存入对应FLASH 区域。 蓝牙模块HC05使用 模块介绍 模块AT指令集 与上位机串口通信 与手机蓝牙调试助手通信 蓝牙模块接收到手机端发来的数据通过串口发送出去在上位机使用串口调试助手可以查看到。 与单片机连接通信
http://www.pierceye.com/news/789416/

相关文章:

  • 用dw怎么做网站首页wordpress 文章页面失败
  • 郑州网站制作专业乐云seowordpress it博客主题
  • 支付宝手机网站支付二维码怎么做网站 开发
  • 教育网站制作视频代理网址ag80hncom
  • 泰兴公司做网站建设制作外贸网站公司
  • 手机wap网站大全作品提示优化要删吗
  • 郑州网站建设技术支持云南澄江县建设局网站
  • wordpress建企业网站设置网站一级域名和二级域名
  • 云南省城乡与住房建设厅网站合肥网红打卡地
  • 用dw做的企业网站宁波seo优化费用
  • 网站制作开发建网站公司 蓝纤科技
  • 怎样到国外做合法网站法网站网站建设小组实验报告
  • DNF做钓鱼网站网站建设方案书编写
  • 提高网站粘性wordpress tag中文
  • 公司已有网站 如何自己做推广wordpress的音乐插件怎么用
  • 权威网站php wordpress 等
  • 建设网站企业公司中通建设计院第四分公司网站
  • 快站免费网站建设哪家好南宁市住房建设局网站
  • 学生做的网站成品roseonly企业网站优化
  • 台前网站建设电话百度账号注册申请
  • 政和县建设局网站公告征婚网站咋做
  • 做网站需要用到的语言注册公司需要多少钱保证金
  • 如何给别人做网站赚钱互联网广告营销方案
  • 上海专业做网站公司地址软文代写
  • 家居网站源码宁波专业建设网站建站公司
  • 成都网站建设 Vr功能 卓 公司网站后台管理系统怎么用
  • 微站网建站系统南昌科技网站建设
  • 商家网站建设模板快速备份wordpress
  • 网站建设推广价格肯德基网站建设
  • 网站开发课程百度云平湖公司做网站