1)实验平台:正点原子MiniPRO STM32H750 开发板
2) 章节摘自【正点原子】《MiniPRO H750开发指南》
3)购买链接:https://detail.tmall.com/item.htm?id=677017430560
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boar ... 32h750_minipro.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子STM32技术交流群:756580169 


第十二章 SYSTEM文件夹介绍
12.2 sys文件夹代码介绍sys文件夹内包含了sys.c和sys.h两个文件,主要实现下面的几个函数,以及一些汇编函数。 - /* 设置中断向量表偏移量 */
- void sys_nvic_set_vector_table(uint32_t baseaddr, uint32_t offset);
- void sys_cache_enable(void); /* 使能STM32H7的L1-Cahce */
- uint8_t sys_stm32_clock_init(uint32_t plln, uint32_t pllm, uint32_t pllp,
- uint32_t pllq); /* 配置系统时钟 */
- void sys_qspi_enable_memmapmode(uint8_t ftype); /* QSPI进入内存映射模式 */
-
- /* 以下为汇编函数 */
- void sys_wfi_set(void); /* 执行WFI指令 */
- void sys_intx_disable(void); /* 关闭所有中断 */
- void sys_intx_enable(void); /* 开启所有中断 */
- void sys_msr_msp(uint32_t addr); /* 设置栈顶地址 */
复制代码sys_nvic_set_vector_table函数用于设置中断向量表偏移地址。sys_stm32_clock_ini函数的讲解请参考11.2.1小节STM32H7时钟系统配置章节内容。 接下来我们重点看一下sys_cache_enable函数和sys_qspi_enable_memmapmode函数。 12.2.1 Cache使能函数 STM32H7自带了指令Cache(I Cache)和数据Cache(D Cache),使用I/D Cache可以缓存指令/数据,提高CPU访问指令/数据的速度,从而大大提高MCU的性能。不过,MCU在复位后,I/D Cache默认都是关闭的,为了提高性能,我们需要开启I/D Cache,在sys.c里面,我们提供了如下函数: - /**
- * @brief 使能STM32H7的L1-Cache, 同时开启D cache的强制透写
- * [url=home.php?mod=space&uid=271674]@param[/url] 无
- * @retval 无
- */
- void sys_cache_enable(void)
- {
- SCB_EnableICache(); /* 使能I-Cache,函数在core_cm7.h里面定义 */
- SCB_EnableDCache(); /* 使能D-Cache,函数在core_cm7.h里面定义 */
- SCB->CACR |= 1 << 2; /* 强制D-Cache透写,如不开启透写,实际使用中可能遇到各种问题 */
- }
复制代码该函数,通过调用SCB_EnableICache和SCB_EnableDCache这两个函数来使能I Cache和D Cache。不过,在使能D Cache之后,SRAM里面的数据有可能会被缓存在Cache里面,此时如果有DMA之类的外设访问这个SRAM里面的数据,就有可能和Cache里面数据不同步,导致数据出错,为了防止这种问题,保证数据的一致性,我们设置了D Cache的强制透写功能(Write Through),这样CPU每次操作Cache里面的数据,同时也会更新到SRAM里面,保证D Cache和SRAM里面数据一致。关于Cache的详细介绍,请参考《STM32F7 Cache Oveview》和《Level 1 cache on STM32F7 Series》(见光盘:8,STM32参考资料 文件夹)。注意:F7和H7这部分知识是通用的,所以参考F7的这两份文档即可,H7的这两个资料暂时没出来。 这里SCB_EnableICache和SCB_EnableDCache这两个函数,是在core_cm7.h里面定义的,我们直接调用即可,另外,core_cm7.h里面还提供了以下五个常用函数: 1,SCB_DisableICache函数,用于关闭I Cache。 2,SCB_DisableDCache函数,用于关闭D Cache。 3,SCB_InvalidateDCache函数,用于丢弃D Cache当前数据,重新从SRAM获取数据。 4,SCB_CleanDCache函数,用于将D Cache数据回写到SRAM里面,同步数据。 5,SCB_CleanInvalidateDCache函数,用于回写数据到SRAM,并重新获取D Cache数据。 在Cache_Enable函数里面,我们直接开启了D Cache的透写模式,这样带来的好处就是可以保证D Cache和SRAM里面数据的一致性,坏处就是会损失一定的性能(每次都要回写数据),如果大家想自己控制D Cache数据的回写,以获得最佳性能,则可以关闭D Cache透写模式,并在适当的时候,调用SCB_CleanDCache、SCB_InvalidateDCache和SCB_CleanInvalidate DCache等函数,这对程序员的要求非常高,程序员必须清楚什么时候该回写,什么时候该更新D Cache!如果能力不够,还是建议开启D Cache的透写,以免引起各种莫名其妙的问题。 12.2.2 QSPI_Enable_Memmapmode函数该函数有三个作用: 1, 初始化QSPI接口,并使能内存映射模式; 2, 初始化外部SPI FLASH,使能QPI(QSPI)模式。 3, 设置QSPI FLASH空间的MPU保护。 其代码如下: - /**
- * @brief QSPI进入内存映射模式(执行QSPI代码必备前提)
- * [url=home.php?mod=space&uid=60778]@note[/url] 必须根据所使用QSPI FLASH的容量设置正确的ftype值!
- * @param ftype: flash类型
- * @arg 0, 普通FLASH, 容量在128Mbit及以内的
- * @arg 1, 大容量FLASH, 容量在256Mbit及以上的.
- * @retval 无
- */
- void sys_qspi_enable_memmapmode(uint8_t ftype)
- {
- uint32_t tempreg = 0;
- GPIO_InitTypeDef qspi_gpio;
-
- __HAL_RCC_GPIOB_CLK_ENABLE(); /* 使能PORTB时钟 */
- __HAL_RCC_GPIOD_CLK_ENABLE(); /* 使能PORTD时钟 */
- __HAL_RCC_GPIOE_CLK_ENABLE(); /* 使能PORTE时钟 */
- __HAL_RCC_QSPI_CLK_ENABLE(); /* QSPI时钟使能 */
-
- qspi_gpio.Pin = GPIO_PIN_6; /* PB6 AF10 */
- qspi_gpio.Mode = GPIO_MODE_AF_PP;
- qspi_gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
- qspi_gpio.Pull = GPIO_PULLUP;
- qspi_gpio.Alternate = GPIO_AF10_QUADSPI;
- HAL_GPIO_Init(GPIOB, &qspi_gpio);
-
- qspi_gpio.Pin = GPIO_PIN_2; /* PB2 AF9 */
- qspi_gpio.Alternate = GPIO_AF9_QUADSPI;
- HAL_GPIO_Init(GPIOB, &qspi_gpio);
-
- qspi_gpio.Pin = GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13;/* PD11,12,13 AF9 */
- qspi_gpio.Alternate = GPIO_AF9_QUADSPI;
- HAL_GPIO_Init(GPIOD, &qspi_gpio);
-
- qspi_gpio.Pin = GPIO_PIN_2; /* PE2 AF9 */
- qspi_gpio.Alternate = GPIO_AF9_QUADSPI;
- HAL_GPIO_Init(GPIOE, &qspi_gpio);
-
- /* QSPI设置,参考QSPI实验的QSPI_Init函数 */
- RCC->AHB3RSTR |= 1 << 14; /* 复位QSPI */
- RCC->AHB3RSTR &= ~(1 << 14); /* 停止复位QSPI */
-
- while (QUADSPI->SR & (1 << 5)); /* 等待BUSY位清零 */
-
- /* QSPI时钟源已经在sys_stm32_clock_init()函数中设置 */
- /* 设置CR寄存器, 这些值怎么来的,请参考QSPI实验/看H750参考手册寄存器描述分析 */
- QUADSPI->CR = 0X01000310;
- /* 设置DCR寄存器(FLASH容量32M(最大容量设置为32M, 默认用16M的), tSHSL=3个时钟) */
- QUADSPI->DCR = 0X00180201;
- QUADSPI->CR |= 1 << 0; /* 使能QSPI */
-
- /*
- * 注意:QSPI QE位的使能,在QSPI烧写算法里面,就已经设置了
- * 所以, 这里可以不用设置QE位,否则需要加入对QE位置1的代码
- * 不过, 代码必须通过仿真器下载, 直接烧录到外部QSPI FLASH, 是不可用的
- * 如果想直接烧录到外部QSPI FLASH也可以用, 则需要在这里添加QE位置1的代码
- *
- * 另外, 对与W25Q256,还需要使能4字节地址模式,或者设置S3的ADP位为1.
- * 我们在QSPI烧写算法里面已经设置了ADP=1(上电即32位地址模式),因此这里也
- * 不需要发送进入4字节地址模式指令/设置ADP=1了, 否则还需要设置ADP=1
- */
-
- /* BY/W25QXX 写使能(0X06指令)*/
- while (QUADSPI->SR & (1 << 5)); /* 等待BUSY位清零 */
-
- QUADSPI->CCR = 0X00000106; /* 发送0X06指令,BY/W25QXX写使能 */
-
- while ((QUADSPI->SR & (1 << 1)) == 0);/* 等待指令发送完成 */
-
- QUADSPI->FCR |= 1 << 1; /* 清除发送完成标志位 */
-
- /* MemroyMap 模式设置 */
- while (QUADSPI->SR & (1 << 5)); /* 等待BUSY位清零 */
-
- QUADSPI->ABR = 0; /* 交替字节设置为0,实际上就是25QXX 0XEB指令的, M0~M7 = 0 */
- tempreg = 0XEB; /*INSTRUCTION[7:0]=0XEB, 发送0XEB指令(Fast Read QUAD I/O)*/
- tempreg |= 1 << 8; /* IMODE[1:0] = 1, 单线传输指令 */
- tempreg |= 3 << 10; /* ADDRESS[1:0] = 3, 四线传输地址 */
- tempreg |= (2 + ftype) << 12; /* ADSIZE[1:0] = 2/3,
- 24位(ftype = 0) / 32位(ftype = 1)地址长度 */
- tempreg |= 3 << 14; /* ABMODE[1:0] = 3, 四线传输交替字节 */
- tempreg |= 0 << 16; /* ABSIZE[1:0] = 0, 8位交替字节(M0~M7) */
- tempreg |= 4 << 18; /* DCYC[4:0] = 4, 4个dummy周期 */
- tempreg |= 3 << 24; /* DMODE[1:0] = 3, 四线传输数据 */
- tempreg |= 3 << 26; /* FMODE[1:0] = 3, 内存映射模式 */
- QUADSPI->CCR = tempreg; /* 设置CCR寄存器 */
-
- /* 设置QSPI FLASH空间的MPU保护 */
- SCB->SHCSR &= ~(1 << 16); /* 禁止MemManage */
- MPU->CTRL &= ~(1 << 0); /* 禁止MPU */
- MPU->RNR = 0; /* 设置保护区域编号为0(1~7可以给其他内存用) */
- MPU->RBAR = 0X90000000; /* 基地址为0X9000 000, 即QSPI的起始地址 */
- MPU->RASR = 0X0303002D; /* 设置保护参数(禁止共用, 允许cache, 允许缓冲),
- 详见MPU实验解析 */
- MPU->CTRL = (1 << 2) | (1 << 0); /* 使能PRIVDEFENA, 使能MPU */
- SCB->SHCSR |= 1 << 16; /* 使能MemManage */
- }
复制代码 以上代码,可以分为4个部分解读: 第一部分,初始化QSPI相关GPIO及QSPI接口。 第二部分,设置QSPI FLASH进入QSPI模式,并设置相关参数,这里使用的是直接操作寄存器的方式,并没有的调用任何函数,目的就是简化代码。这部分代码的详细理解,读者可以参考我们后续的QSPI实验(实验24 QSPI实验)的相关介绍。这里只需要直接调用就好。 第三部分,设置STM32H750的QSPI的内存映射模式,从而可以执行外部SPI FLASH的代码。 第四部分,设置QSPI FLASH空间的内存保护。 为了简化sys_qspi_enable_memmapmode函数,我们很多操作都做了精简处理,所以理解起来会有一定困难,如果实在看不懂,可以绕过这个,先学习后面的知识点,回头再来学习该函数的具体实现。 需要注意的是:在sys_qspi_enable_memmapmode函数之前的所有代码,必须不能放到外部QSPI FLASH,因为在该函数之前,STM32H750都是无法访问外部QSPI FLASH的,所以如果遇到一些代码启动不了的情况,请重点怀疑是不是在该函数之前,就调用了QSPI FLASH的程序?如果有,需要将这部分代码放到该函数之后才行!或者将这部分代码放内部FLASH运行(方法见3.2.8节)。 函数/指令地址,我们可以通过仿真,看反汇编窗口(Disassembly),会有地址显示,从而快速找到问题。 12.3 usart文件夹代码介绍该文件夹下面有usart.c和usarts.h两个文件。在我们的工程使用串口1和串口调试助手来实现调试功能,可以把单片机的信息通过串口助手显示到电脑屏幕。串口相关知识,我们将在第十七章讲解串口实验的时候给大家详细讲解。本节我们只给大家讲解比较独立的printf函数支持相关的知识。 12.3.1 printf函数支持 在我们学习C语言时,可以通过printf函数把需要的参数显示到屏幕上,可以做一些简单的调试信息,但对于单片机来说,如果想实现类似的功能来用printf辅助调试的话,是否有办法呢?有,这就是这一节要讲的内容。 标准库下的printf为调试属性的函数,如果直接使用,会使单片机进入半主机模式(semihosting),这是一种调试模式,直接下载代码后出现程序无法运行,但是在Debug模式下程序可能正常工作的情况。半主机是ARM目标的一种机制,用于将输入/输出请求从应用程序代码通信到运行调试器的主机。例如,此机制可用于允许C库中的函数(如printf()和scanf())使用主机的屏幕和键盘,而不是在目标系统上设置屏幕和键盘。这很有用,因为开发硬件通常不具有最终系统的所有输入和输出设备,如屏幕、键盘等。半主机是通过一组定义好的软件指令(如SVC)SVC指令(以前称为SWI指令)来实现的,这些指令通过程序控制生成异常。应用程序调用相应的半主机调用,然后调试代理处理该异常。调试代理(这里的调试代理是仿真器)提供与主机之间的必需通信。也就是说使用半主机模式必须使用仿真器调试。 如果想在独立环境下运行调试功能的函数,我们这里是printf ,printf对字符ch处理后写入文件f,最后使用fputc将文件f输出到显示设备。对于PC端的设备,fputc通过复杂的源码,最终把字符显示到屏幕上。那我们需要做的,就是把printf调用的fputc函数重新实现,重定向fputc的输出,同时避免进入半主模式。 要避免半主机模式,现在主要有两种方式:一是使用MicroLib,即微库;另一种方法是确保ARM应用程序中没有链接MicroLib的半主机相关函数,我们要取消ARM的半主机工作模式,这可以通过代码实现。 先说微库,ARM的C微库MicroLib是为嵌入式设备开发的一套类似于标准C接口函数的精简代码库,用于替代默认C库,是专门针对专业嵌入式应用开发而设计的,特别适合那些对存储空间有特别要求的嵌入式应用程序,这些程序一般不在操作系统下运行。使用微库编写程序要注意其与默认C库之间存在的一些差异,如main()函数不能声明带参数,也无须返回;不支持stdio,除了无缓冲的stdin、stdout和syderr; 微库不支持操作系统函数;微库不支持可选的单或两区存储模式;微库只提供分离的堆和栈两区存储模式等等,它裁减了很多函数,而且还有很多东西不支持。如果原来用标准库可以跑,选择MicroLib后却突然不行了,是很常见的。与标准的C库不一样,微库重新实现了printf,使用微库的情况下就不会进入半主机模式了。Keil下使用微库的方法很简单,在“Target”下勾选“Use MicroLib”即可。 图12.3.1.1 MDK工程下使用微库的方法 在keil5中,不管是否使用半主机模式,使用printf,scanf,fopen,fread等都需要自己填充底层函数,以printf为例,需要补充定义fputc,启用微库后,在我们初始化和使能串口1之后,我们只需要重新实现fputc的功能即可将每个传给fputc函数的字符ch重定向到串口1,如果这时接上串口调试助手的话,可以看到串口的数据。实现的代码如下: - /* 重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
- int fputc(int ch, FILE *f)
- {
- while ((USART_UX->ISR & 0X40) == 0); /* 等待上一个字符发送完成 */
-
- USART_UX->TDR = (uint8_t)ch; /* 将要发送的字符 ch 写入到DR寄存器 */
- return ch;
- }
复制代码上面说到了微库的一些限制,使用时注意某些函数与标准库的区别就不会影响到我们代码的正常功能。如果不想使用微库,那就要用到我们提到的第二种方法:取消ARM的半主机工作模式;只需在代码中添加不使用半主机的声明即可,对于AC5和AC6编译器版本,声明半主机的语法不同,为了同时兼容这两种语法,我们在利用编译器自带的宏__ARMCC_VERSION判定编译器版本,并根据版本不同选择不同的语法声明不使用半主机模式,具体代码如下: - #if (__ARMCC_VERSION >= 6010050) /* 使用AC6编译器时 */
- __asm(".global __use_no_semihosting\n\t"); /* 声明不使用半主机模式 */
- __asm(".global __ARM_use_no_argv \n\t"); /* AC6下需要声明main函数为无参数格式,否则部分例程可能出现半主机模式 */
-
- #else
- /* 使用AC5编译器时, 要在这里定义__FILE 和 不使用半主机模式 */
- #pragma import(__use_no_semihosting)
-
- /* 解决HAL库使用时, 某些情况可能报错的bug */
- struct __FILE
- {
- int handle;
- /* Whatever you require here. If the only file you are using is */
- /* standard output using printf() for debugging, no file handling */
- /* is required. */
- };
-
- #endif
复制代码使用的上面的代码,Keil的编译器就不会把标准库的这部分函数链接到我们的代码里。如果用到原来半主机模式下的调试函数,需要重新实现它的一些依赖函数接口,对于printf函数需要实现的接口,我们的代码中将它们实现如下: - /* 不使用半主机模式,至少需要重定义_ttywrch\_sys_exit\_sys_command_string函数,以同时兼容AC6和AC5模式 */
- int _ttywrch(int ch)
- {
- ch = ch;
- return ch;
- }
- /* 定义_sys_exit()以避免使用半主机模式 */
- void _sys_exit(int x)
- {
- x = x;
- }
- char *_sys_command_string(char *cmd, int len)
- {
- return NULL;
- }
- /* FILE 在 stdio.h里面定义. */
- FILE __stdout;/* 不使用半主机模式,至少需要重定义_ttywrch\_sys_exit\_sys_command_string函数,以同时兼容AC6和AC5模式 */
- int _ttywrch(int ch)
- {
- ch = ch;
- return ch;
- }
- /* 定义_sys_exit()以避免使用半主机模式 */
- void _sys_exit(int x)
- {
- x = x;
- }
- char *_sys_command_string(char *cmd, int len)
- {
- return NULL;
- }
- /* FILE 在 stdio.h里面定义. */
- FILE __stdout;
复制代码fputc的重定向和之前一样,重定向到串口1即可,如果硬件资源允许,读者有特殊需求,也可以重定向到LCD或者其它串口。 - /* 重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
- int fputc(int ch, FILE *f)
- {
- while ((USART_UX->SR & 0X40) == 0); /* 等待上一个字符发送完成 */
- USART_UX->DR = (uint8_t)ch; /* 将要发送的字符 ch 写入到DR寄存器 */
- return ch;
- }
复制代码
|