STM32

在 Cube 软件包中,为不同系列 MCU、不同外设提供了对应的例程方便开发参考。其中,针对STM324xG-EVAL 平台提供了 UART 中断发送接收的例程。开发者参考了这个例程进行 UART 功能开发,并且为了实现不间断的接收功能,在接收回调函数中,再次调用中断接收函数。在这种情况下,出现了例程执行异常。本文分析了这种情况出现原因及解决方法。

问题描述

测试验证板: STM3240G-EVAL
参考例程路径:
STM32Cube_FW_F4_V1.15.0\Projects\STM324xG_EVAL\Examples\UART\UART_Hyperterminal_IT

基于上述例程,出于前言中交代的应用目的,在接收回调函数 HAL_UART_RxCpltCallback 中,再次调用 HAL_UART_Receive_IT。
随后出现例程执行卡死在下面红色标识的语句处,导致了 UART 中断发送无法正确被执行。

 if(HAL_UART_Receive_IT(&UartHandle, (uint8_t *)aRxBuffer, RXBUFFERSIZE) != HAL_OK)
 {
 /* Turn LED3 on: Transfer error in reception process */
 BSP_LED_On(LED3);
 while(1)
 {
 }
 }
 /*##-4- Wait for the end of the transfer ###################################*/
 /* Before starting a new communication transfer, you need to check the current
 state of the peripheral; if it 抯 busy you need to wait for the end of current
 transfer before starting a new one.
 For simplicity reasons, this example is just waiting till the end of the
 transfer, but application may perform other tasks while transfer operation
 is ongoing. */
 while (HAL_UART_GetState(&UartHandle) != HAL_UART_STATE_READY)
 {
 }
 /*##-5- Send the received Buffer ###########################################*/
 if(HAL_UART_Transmit_IT(&UartHandle, (uint8_t*)aRxBuffer, RXBUFFERSIZE)!= HAL_OK)

问题分析及解决

根据描述,首先考虑到是否由于 UART 始终处于接收忙状态,导致中断发送收到影响。但是 UART 外设具有发送数据寄存器和接收数据寄存器,以及互不影响的接收、发送中断。

继续对问题进行定位,发现在 HAL_UART_GetState 函数中,会同时获取发送和接收状态。这意味着,只有在发送和接收同时处于就绪状态时,中断发送函数才会被执行。而开发者的应用实现中,使得UART 始终处于接收状态,从而判断无法通过。

清楚了产生原因后,问题解决就一目了然了。只需将对发送和接收状态的判断,改写成仅对发送的状
态的判断,如下所示。其中 gState 对应着发送状态。

//while (HAL_UART_GetState(&UartHandle) != HAL_UART_STATE_READY)
 while( UartHandle.gState != HAL_UART_STATE_READY)

总结

在使用 Cube 软件包例程时,最好能够对各驱动接口函数有一定认识。例如上述问题,对于例程来说,没有问题。但是转移到应用时,就需要考虑到例程中调用的判断函数是否符合应用目的了。

而对于各驱动接口函数,在函数定义处,都给出了介绍,包括各参数说明。同时,在 Cube 软件包的Drivers 目录下,提供了对驱动接口函数介绍的文档。

来源:ST

围观 2
8

前言

STM32 提供了灵活多样的外扩存储器访问实现。本文中,介绍如何利用 QSPI (QuadSPI) 外扩串行
NOR Flash 存储器。首先对 QSPI 接口功能特性进行介绍,然后分别介绍硬件设计和软件开发。并基于 STM32CubeMX,提供访问 MICRON N25Q128A13EF840F 的实现参考。

一 实现环境

开发板:STM32F469G-DISCO
开发库:STM32CubeF4 v1.16.0
STM32CubeMX: v4.22.0
集成开发环境:IAR v7.70.1.11486

实现过程在 STM32F469I-DISCO 板上展开,利用板上已有的串行 NOR Flash 存储器(MICRON
N25Q128A13EF840F)。呈现整个开发涉及环节。在本文中,首先根据 QSPI 接口,介绍 QSPI 与外扩串行存储器硬件连接。

另外,Cube 软件包中包含 QSPI 实现例,在本文对库中实现的 QSPI 例不做讨论,读者也可参考这些 QSPI 例进行设计。本文围绕由 STM32CubeMX 生成的工程,介绍如何实现对外扩串行 NOR Flash 存储器的访问。

二 QSPI 介绍

在呈现 QSPI 访问外扩 Flash 的实现例前, 需要对 QSPI 有一定的了解,在此对 QSPI 进行简短的介绍。更多内容请参考AN4760。

QSPI(Quad-SPI)支持四线串行访问形式。同时,QSPI 支持传统 SPI 和 Dual-SPI 模式,Dual-SPI 模式支持两线串行访问。与 FMC/FSMC 比较,QSPI 支持更低成本、更小封装外部串行 Flash 存储器,更少的 IO 引脚占用,有效减少 PCB 面积,降低 PCB 设计复杂度。

QSPI 在不同系列 STM32 产品线的支持情况(仅部分罗列,未涵盖所有支持型号)。

利用 QuadSPI 外扩串行 NOR Flash 的实现

QSPI 接口提供了灵活可配置的 5 个阶段,如下图所示(仅用于理解阶段构成,时序图根据配置不同存在差异)。分别是命令阶段、地址阶段、复用字节阶段、Dummy 阶段和数据阶段。可以根据外扩 Flash 中命令时序对不同阶段进行配置。后续会以实例进行呈现。更多内容请参考 AN4760。
利用 QuadSPI 外扩串行 NOR Flash 的实现

QSPI 支持三种模式,分别是:

间接模式 - 所有操作通过 QSPI 寄存器实现,类似于传统 SPI,可以使用阻塞模式、中断模式或者 DMA 模式进行读写等访问。本文中提供的实现例为间接模式下的实现。

状态轮询模式 - 接口自动轮询指定寄存器,直到回读寄存器内容与指定条件匹配。可应用于状态检测,从而实现忙等待等效果。本文不对此模式进行实现介绍,应用实现可参考 Cube 软件包中 QSPI 例程。

存储器映射模式 - 外扩 Flash 被视为内部存储器,支持 AHB 主器件直接访问,CPU 能够直接运行位于 QSPI 存储器的执行代码。内部系统架构如下图所示(以 STM32F469/F479 为例)。本文不对此模式进行实现介绍,应用实现可参考 Cube软件包中 QSPI 例程 QSPI_ExecuteInPlace。

利用 QuadSPI 外扩串行 NOR Flash 的实现

三 QSPI 外扩串行 Flash 实现例

3.1 串行 Flash 介绍

以 MICRON N25Q128A13EF840F 为例,更多细节请参考存储器手册。N25Q128A13EF840F 引脚图、时序图和电气参数来源于 N25Q128A13 手册文档。

支持协议: SPI, Dual I/O(对应 Dual-SPI), Quad I/O(对应 Quad-SPI)
支持访问模式: 单线访问、双线访问、四线访问,得益于 QSPI 接口的灵活可配性,三种访问模式全部支持。
供电电压范围: 2.7 ~ 3.6V
最大时钟频率: 108MHz
存储空间: 128Mb (16MB)

器件引脚示意图如下所示。由两根电源引脚和六根 QSPI 信号线构成。

利用 QuadSPI 外扩串行 NOR Flash 的实现

下表为存储器 N25Q128A13xxx 命令(未列出全部命令)。通过下表可知,存储器提供了灵活的访问实现形式,而结合同样灵活可配的 QSPI 接口,能够实现存储器命令全支持。而本文仅为提供设计思路,呈现了部分命令在 QSPI 上的实现。

利用 QuadSPI 外扩串行 NOR Flash 的实现

3.2 硬件设计

涉及到的信号线少,硬件设计简单,只需直接将 QSPI 的六根信号线与存储器连接即可。考虑到可测性,可以增加串行电阻或者测试点。硬件电路图如下所示。

利用 QuadSPI 外扩串行 NOR Flash 的实现

QSPI 接口 PCB 设计遵循如下几点,更多硬件设计内容请参考 AN4488。

a. 线阻 50Ω ±10%
b. 最大线长 c. 避免在不同信号层走信号线
d. 时钟线至少离其他信号线 3 倍线宽距离
e. 数据信号线长差 ≤ 10mm
f. 避免时钟线采用蛇形走线,同时尽量减少数据线上过孔。

3.3 软件开发流程

利用 QuadSPI 外扩串行 NOR Flash 的实现

3.4 软件实现例

在环境搭建完成后,就可以利用 STM32CubeMX 根据硬件连接情况,进行 QSPI 配置,获取 IAR 工程。具体软件实现流程如下。

利用 QuadSPI 外扩串行 NOR Flash 的实现

a. 利用 STM32CubeMX 生成 IAR 工程
打开 STM32CubeMX  点击”New project”  在”Part Number Search’中输入 STM32F469NI  点击”MCUs Liast”中出现的 STM32F469NIHx  点击“Start Project”  此时,基于 STM32F469NIHx 的 STM32CubeMX 工程被打开。

如下,根据 STM32F469I-DISCO 板硬件连接情况(QSPI NCS, CLK, Q0, Q1, Q2, Q3 对应 PB6, PF10, PF8, PF9,PF7, PF6;外部高速晶振为 8MHz 无源晶振;调试接口采用 SWD 接口,其中 SWCLK, SWDIO 对应 PA14, PA13):选择” QuadSPI”为”Bank1 with Quad SPI Lines”(注:也可在开发过程中,先用 STM32CubeMX 查看 QSPI 接口对应的 IO 引脚,进行硬件开发) 。

注: 在如上选择后,右侧引脚图中 QSPI 对应的引脚会呈现绿色显示。需要根据电路图中所连接的 QSPI 引脚,进行复用引脚确认。例如,在默认情况下,QSPI IO0 对应到 PC9 引脚,而 STM32F469I-DISCO 板上的 QSPI IO0 与PF8 连接,并非 PC9。所以,需要在右侧引脚图中,按住 Ctrl 键,左键在 PC9 引脚按下,拖动至 PF8 处,松开左键和 Ctrl 键,实现 IO0 引脚的关联。

选择“High Speed Clock(HSE)”为“Crystal/Ceramic Resonator”。

选择”Debug”为”Serial Wire”

利用 QuadSPI 外扩串行 NOR Flash 的实现

时钟配置如下图所示。设置输入时钟频率为 8MHz  选择”HSE”做为 PLL 倍频时钟源  选择”PLLCLK”做为主频时钟源  设置 “HCLK”为 180MHz (FAHB 为 180MHz) 点击 Enter 键,自动生成对应主频的时钟参数(仅提供时钟配置参考,并不限制一定要设置 180MHz 主频)。

利用 QuadSPI 外扩串行 NOR Flash 的实现

QSPI 配置如下图。参数的配置需要与存储器参数匹配。

• FIFO Threshold(FIFO 阈值) 配置为 4,并不严格要求。
• Smaple shift 选择“Sample shifting half cycle”。延后半个时钟,获取数据线上数据。可以使用在由于线路设计,数据信号存在较大延迟的场景。

利用 QuadSPI 外扩串行 NOR Flash 的实现

使能 QSPI 中断。

利用 QuadSPI 外扩串行 NOR Flash 的实现

点击菜单栏”Project”  “Settings”  设置”Project Name” , “Project Location” 和 “Toolchain / IDE” 。其中“Toolchain/ IDE”设置为 EWARM 以便生成对应 IDE 的工程。其他选项保持默认。

点击菜单栏”Project”  “Generate Code”  等待 IAR 工程生成,出现”Code Generation”界面  点击”Open Project” 打开工程。

b. 完善工程。
由上述步骤获得的 IAR 工程中,包含了时钟配置及 QSPI 接口的初始化。对于外扩 Flash 的操作,还需要 添加外扩 Flash 支持的命令进行操作。N25Q128A13EF840F 支持的部分命令可参见本文 3.1 小结。

在这里出于简化考虑,仅提供了阻塞式读取 ID,擦除 Flash,块写和快读操作的实现。更多实现模式,可以参考Cube 软件包中提供的 QSPI 例程。

在 N25Q128A13EF840F 手册中提供了读 ID 命令时序,如下图所示。

利用 QuadSPI 外扩串行 NOR Flash 的实现

由时序图可知,读 ID 时序构成: 命令阶段 + 数据阶段。命令阶段和数据阶段线宽都为 1,读 ID 命令码为 0x9E 或者0x9F,ID 数据长度为 17 字节。

在 N25Q128A13EF840F 手册中提供了写使能命令时序,如下图所示。

利用 QuadSPI 外扩串行 NOR Flash 的实现

由时序图可知,块擦除时序仅有命令阶段。命令阶段线宽为 1,写使能命令码为 0x06。(注:这里仅呈现了单线命令模式的实现。除此之外,STM32 QSPI 接口和外扩存储器支持双线、四线模式)。

在 N25Q128A13EF840F 手册中提供了扇区擦除命令时序,如下图所示。

利用 QuadSPI 外扩串行 NOR Flash 的实现

由时序图可知,扇区擦除时序构成:命令阶段+地址阶段。命令阶段和地址阶段线宽为 1,扇区擦除命令码为 0xD8。

其中地址为 24-bit,任一位于需要进行擦除操作的扇区范围内地址都有效。在此简单选择扇区 0 进行擦除,选择地址为 0。(注:这里仅呈现了单线命令模式的实现。除此之外,STM32 QSPI 接口和外扩存储器支持双线、四线模式)。

在 N25Q128A13EF840F 手册中提供了四线快速写命令时序,如下图所示。

利用 QuadSPI 外扩串行 NOR Flash 的实现

由时序图可知,四线快速写命令时序构成:命令阶段+地址阶段+数据阶段。命令阶段和地址阶段线宽为 1,数据阶段线宽为 4,四线快速写命令码为 0x32。其中地址为 24-bit,对应写入起始地址。(注:这里仅呈现了单线命令模式的实现。除此之外,STM32 QSPI 接口和外扩存储器支持双线、四线模式)。

在 N25Q128A13EF840F 手册中提供了四线快速读命令时序,如下图所示。

利用 QuadSPI 外扩串行 NOR Flash 的实现

由时序图可知,四线快速读命令时序构成:命令阶段+地址阶段+数据阶段。命令阶段和地址阶段线宽为 1,数据阶段线宽为 4,四线快速读命令码为 0x6B。其中地址为 24-bit,对应写入起始地址。四线快速读命令默认 Dummy cycles 为 8。(注:这里仅呈现了单线命令模式的实现。除此之外,STM32 QSPI 接口和外扩存储器支持双线、四线模式)。

在 main.c \ main 函数中,增加代码如下。

 … //系统、时钟、IO 和 QSPI 初始化
 /* USER CODE BEGIN 2 */
 QSPI_CommandTypeDef sCommand;
 static uint8_t Buf_ID[17] = {0};
 static uint8_t TxBuf[0x10] = "Ext Flash", RxBuf[0x10] = {0};

 sCommand.DdrMode = QSPI_DDR_MODE_DISABLE; //DDR 模式失能
 sCommand.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY; //DDR 模式下,数据延迟输出
 sCommand.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; //每次发送都包含命令阶段

 /***** 读 ID 操作 *****/
 sCommand.Instruction = 0x9F; //READ ID 命令码
 sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; //命令线宽
 sCommand.AddressMode = QSPI_ADDRESS_NONE; //地址线宽。无地址阶段
 sCommand.DataMode = QSPI_DATA_1_LINE; //数据线宽
 sCommand.NbData = 17; //读取数据长度。ID 长度为 17 字节
 sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; //无复用字节阶段
 sCommand.DummyCycles = 0; //无 Dummy 阶段
 //配置命令(在有数据阶段时,命令在后续发送/接收 API 调用时发送)
 if (HAL_QSPI_Command(&hqspi, &sCommand, 5000) != HAL_OK)
 {
 Error_Handler();
 }
 //执行 QSPI 接收
 if (HAL_QSPI_Receive(&hqspi, Buf_ID,5000) != HAL_OK)
 {
 Error_Handler();
 }
 HAL_Delay(1); //延时 1ms. 单位为 SysTick 定时中断周期

 /***** 写使能操作(需要在块擦除之前,使外扩存储器处于写使能状态) *****/
 sCommand.Instruction = 0x06; //写使能 命令码
 sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; //命令线宽
 sCommand.AddressMode = QSPI_ADDRESS_NONE; //地址线宽。无地址阶段
 sCommand.DataMode = QSPI_DATA_NONE; //数据线宽。无数据阶段
 sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; //无复用字节阶段
 sCommand.DummyCycles = 0; //无 Dummy 阶段
 //配置发送命令
 if (HAL_QSPI_Command(&hqspi, &sCommand, 5000) != HAL_OK)
 {
 Error_Handler();
 }

 /***** 块擦除操作 *****/
 sCommand.Instruction = 0xD8; //扇区擦除 命令码
 sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; //命令线宽
 sCommand.AddressMode = QSPI_ADDRESS_1_LINE; //地址线宽。无地址阶段
 sCommand.AddressSize = QSPI_ADDRESS_24_BITS; //地址长度
 sCommand.Address = 0; //位于所需擦除扇区内的任一地址。
 sCommand.DataMode = QSPI_DATA_NONE; //数据线宽。无数据阶段
 sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; //无复用字节阶段
 sCommand.DummyCycles = 0; //无 Dummy 阶段
 //配置发送命令
 if (HAL_QSPI_Command(&hqspi, &sCommand, 5000) != HAL_OK)
 {
 Error_Handler();
 }
 HAL_Delay(3000); //延时 3s. 单位为 SysTick 定时中断周期

 /***** 写使能操作(需要在块擦除之前,使外扩存储器处于写使能状态) *****/
 sCommand.Instruction = 0x06; //写使能 命令码
 sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; //命令线宽
 sCommand.AddressMode = QSPI_ADDRESS_NONE; //地址线宽。无地址阶段
 sCommand.DataMode = QSPI_DATA_NONE; //数据线宽。无数据阶段
 sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; //无复用字节阶段
 sCommand.DummyCycles = 0; //无 Dummy 阶段
 //配置发送命令
 if (HAL_QSPI_Command(&hqspi, &sCommand, 5000) != HAL_OK)
 {
 Error_Handler();
 }

 /***** 四线快速写操作 *****/
 sCommand.Instruction = 0x32; //四线快速写 命令码
 sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; //命令线宽
 sCommand.AddressMode = QSPI_ADDRESS_1_LINE; //地址线宽
 sCommand.AddressSize = QSPI_ADDRESS_24_BITS; //地址长度
 sCommand.Address = 0; //写入起始地址
 sCommand.DataMode = QSPI_DATA_4_LINES; //数据线宽
 sCommand.NbData = 10; //写入数据长度
 sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; //无复用字节阶段
 sCommand.DummyCycles = 0; //无 Dummy 阶段
 //配置命令(在有数据阶段时,命令在后续发送/接收 API 调用时发送)
 if (HAL_QSPI_Command(&hqspi, &sCommand, 5000) != HAL_OK)
 {
 Error_Handler();
 }
 //执行 QSPI 接收
 if (HAL_QSPI_Transmit(&hqspi, TxBuf ,5000) != HAL_OK)
 {
 Error_Handler();
 }
 HAL_Delay(5); //延时 5ms. 单位为 SysTick 定时中断周期

 /***** 四线快速读操作 *****/
 sCommand.Instruction = 0x6B; //四线快速读 命令码
 sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; //命令线宽
 sCommand.AddressMode = QSPI_ADDRESS_1_LINE; //地址线宽
 sCommand.AddressSize = QSPI_ADDRESS_24_BITS; //地址长度
 sCommand.Address = 0; //起始地址
 sCommand.DataMode = QSPI_DATA_4_LINES; //数据线宽
 sCommand.NbData = 10; //读取数据长度
 sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; //无复用字节阶段
 sCommand.DummyCycles = 8; //Dummy 阶段。N25Q128A13EF840F
Dummy cycles 默认为 15
 //配置命令(在有数据阶段时,命令在后续发送/接收 API 调用时发送)
 if (HAL_QSPI_Command(&hqspi, &sCommand, 5000) != HAL_OK)
 {
 Error_Handler();
 }
 //执行 QSPI 接收
 if (HAL_QSPI_Receive(&hqspi, RxBuf,5000) != HAL_OK)
 {
 Error_Handler();
 }
 /* USER CODE END 2 */

四 小结

STM32 的 QuadSPI 接口灵活可配,对于命令阶段、地址阶段、复用字节阶段、Dummy 阶段和数据阶段都可以进行配置。基于这种灵活性,能够实现市面上 SPI、Dual IO、Quad IO 的串行 Flash 支持。但出于简化考虑,QSPI 支持的中断访问及DMA 访问等更多功能没有在本文进行介绍,更多实现可以参考 ST 提供的 Cube 软件包中的 QSPI 例程。另外,不同厂家的串行 Flash 命令及操作实现略有差异,具体以采用的 Flash 文档描述为准。

相关文档
AN4760 Quad-SPI (QSPI) interface on STM32 microcontrollers
AN4488 Getting started with STM32F4xxxx MCU hardware development
RM0386 STM32F469xx and STM32F479xx advanced ARM®-based 32-bit MCUs

相关工具&链接
STM32CubeMX http://www.st.com/content/st_com/en/products/development-tools/software-...
STM32CubeF4 http://www.st.com/content/st_com/en/products/embedded-software/mcus-embe...

来源: ST

围观 7
17

RS485通信想必大家都知道,在学习RS232时,都会拿485(RS485下文就用485代替)和其作对比。485优缺点不说,网上有。

我用的是STM32库函数学的485通信,所以接下来就讲讲STM32串口实现485双机通信的原理:

485和232都是基于串口的通讯接口,在数据的收发操作上都是一致的。但是他两的通讯模式却大不相同~!232是全双工(例:A->B的同时B->A,瞬时同步)工作模式,而485是半双工(发时不能收,收时不能发)工作模式。在232通信中,主机在发送数据的同时可以收到从机发过来的数据;但在485通信中,收发要经过模式位的切换来进行,譬如,发送数据时,会把模式为置‘1’,表示为发送模式,此时不能接收;当接收数据时,会把模式位置‘0’,表示为接收模式,此时不能发送。

在讲STM32串口实现485双机通信的原理之前,先来复习一下串口中的中断知识点:

串口的中断类型有很多种。这里主要讲两种:接收到数据中断和发送数据完成中断。这两个中断跟两个标志位有密切关系:RXNE(读数据寄存器非空)和TC(发送完成)。譬如在接收到数据的时候(RXNE,读数据寄存器非空),我们要产生中断。在发送数据结束的时候(TC,发送完成)要产生中断。这两种中断的产生方法都是在开启串口中断函数(USART_ITConfig();)中配置。并在获取中断状态函数(USART_ITStatus();)中判断是发送中断还是接受中断。

注意:1、有人问当产生接收中断时,没看到程序中清除中断标志啊,那不就一直中断下去了吗?这里官方规定有两种方法清除中断标志(书里面也有):
(1)尽快读取USART_DR(数据寄存器),通过读USART_DR可以将该位清零;
(2)可以直接向该位写‘0’,直接清零。

2、两块板子的串口波特率一定要相同,否则将得不到想要的数据。

STM32串口实现485双机通信的原理(库函数):

1、在主函数中,扫描按键。一旦key0按下,首先将5个字节的数据存入rs485buf[]数组中,然后调用RS485_Send_Data(rs485buf,5);函数将5个字节的数据发送到串口。

当STM32遇到串口RS485双机通信,这样处理最便捷

2、进入RS485_Send_Data();函数中,先通过标志位将485设置为发送模式(RS485_TX_EN=1),然后循环5次,将这5个字节数据,通过库函数:USART_Send_Data();发送到USART_DR寄存器(自动发送),最后通过标志位将485设置为接收模式(RS485_TX_EN=0)。

当STM32遇到串口RS485双机通信,这样处理最便捷

 
3、前两个是发送。现在是接收了。主函数中通过RS485_Receive_Data();不停的接收。

4、进入RS485_Receive_Data();函数中,

如果一直没有按按键,则不会发送数据,也就不会产生接收中断,也就不会往我们定义的接收缓冲区(RS485_RX_BUF[64])里写数据。RS485_RX_CNT计数器的值也就是‘0’,对应在RS485_Receive_Data();函数中就不会进入里面的if语句(下图中红框)

当STM32遇到串口RS485双机通信,这样处理最便捷

如果按键按下了,就会发送数据,就会产生接收中断,就会往我们定义的接收缓冲区(RS485_RX_BUF[64])里写数据,RS485_RX_CNT计数器的值也就开始自增,对应在RS485_Receive_Data();函数中就会进入里面的if语句,从之前定义的接收缓冲区(RS485_RX_BUF[64])里面取数据。

5、最后就将取出来的数据(接收到的数据)显示出来。

转自: 与非网

围观 10
14

1 前言

在进行 USB 开发的过程中,有多个客户反馈,USB 传输数据时出现卡顿现象。本文将针对这
一问题进行分析。

2 问题分析

这几个客户问题现象基本差不多,采用 STM32 作为 Device 设备,在与上位机或者 PC 端双向通讯一段时间后,从 Device 端到 Host 端的数据能够正常,而从 Host 端到 Device 端的数据异常,也就是说,STM32 在一段时间后不再能正常接收数据,但是,如果只是单向通信,就一直都是正常的。

这几个客户,有用 STM32F2 的,也有用 STM32F4 的,有用 CDC 类的,也有用作 HID 设备的,但都使用了 Cube 库。

下面就具体问题以其中一个客户使用 STM32F411 的 USB CDC 类的案例来分析问题,现象如
下 USB 通讯数据(CDC 类):

STM32单片机USB传输数据时出现卡顿现象

展开 Data Out 数据:

STM32单片机USB传输数据时出现卡顿现象

分析上图发现,并不是 Host 端没有向 Device 端发送 Data Out 数据,而是确实发送了,但被 Device
端 NAK 了。那么为什么会被 NAK 呢?

通过在调试下查看寄存器,我们发现当出现问题时,Data OUT 对应的端点 1 是处于关闭状态,那么为
什么端点 1 会关闭?查看 STM32 端的接收代码:

usbd_cdc_if.c:

static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len)
{
   /* USER CODE BEGIN 6 */
   //USBTask_ReceiveMsg(Buf, *Len); //UserRxBufferFS
   USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
   USBD_CDC_ReceivePacket(&hUsbDeviceFS);
   return (USBD_OK);
   /* USER CODE END 6 */
}

如上代码,在 MCU 端接收到赖在 Host 端的数据后不做任何处理就立即接收下一次数据传输,问题是,这里对接收到的数据啥也没有做,居然还会出现 Data Out 端点关闭的问题,那么 OUT 端点到底是怎么关闭的呢?我们接下来看子函数:

CDC_Receive_FS() ->USBD_CDC_ReceivePacket() ->USBD_LL_PrepareReceive() -
>HAL_PCD_EP_Receive() ->USB_EPStartXfer()

最后在 USB_EPStartXfer 函数中有发现再次使能 OUT 端点的代码:

//…
 else /* OUT endpoint */
 {
    /* Program the transfer size and packet count as follows:
    * pktcnt = N
    * xfersize = N * maxpacket
    */
    USBx_OUTEP(ep->num)->DOEPTSIZ &= ~(USB_OTG_DOEPTSIZ_XFRSIZ);
    USBx_OUTEP(ep->num)->DOEPTSIZ &= ~(USB_OTG_DOEPTSIZ_PKTCNT);
    if (ep->xfer_len == 0U)
    {
       USBx_OUTEP(ep->num)->DOEPTSIZ |= (USB_OTG_DOEPTSIZ_XFRSIZ & ep->maxpacket);
       USBx_OUTEP(ep->num)->DOEPTSIZ |= (USB_OTG_DOEPTSIZ_PKTCNT & (1U xfer_len + ep->maxpacket -1U)/ ep->maxpacket;
       USBx_OUTEP(ep->num)->DOEPTSIZ |= (USB_OTG_DOEPTSIZ_PKTCNT & (pktcnt num)->DOEPTSIZ |= (USB_OTG_DOEPTSIZ_XFRSIZ & (ep->maxpacket *
      pktcnt));
    }
    if (dma == 1U)
    {
       USBx_OUTEP(ep->num)->DOEPDMA = (uint32_t)ep->xfer_buff;
    }

    if (ep->type == EP_TYPE_ISOC)
    {
       if ((USBx_DEVICE->DSTS & ( 1U num)->DOEPCTL |= USB_OTG_DOEPCTL_SODDFRM;
       }
       else
       {
          USBx_OUTEP(ep->num)->DOEPCTL |= USB_OTG_DOEPCTL_SD0PID_SEVNFRM;
       }
    }
     /* EP enable */
     USBx_OUTEP(ep->num)->DOEPCTL |= (USB_OTG_DOEPCTL_CNAK | USB_OTG_DOEPCTL_EPENA);
 }

也就是说,在调用这个函数之前这个 OUT 端点原本就是关闭的?真的吗?这怎么跟我们理解的不一样?
不应该是 OUT 端点一旦打开就一直开着的吗?带着这些疑问,我们查看 STM32F411 的参考手册,终
于在 22.17.6 Operational model 一节中找到这么一幅图(由于 CDC 类数据传输采用的是 BULK 传输):

STM32单片机USB传输数据时出现卡顿现象

如上图,MCU 对 BULK 类型的 OUT 数据处理例程大体如下:

1> Host 端试图向一个端点发送 OUT token;
2> 当 Device 端的 USB 外设接收到这么一个 OUT token 后,如果 RXFIFO 空间足够,它将数据包存
储到 RXFIFO 中;
3> 在将数据包内容存储到 RXFIFO 后,USB 外设将产生一个 RXFLVL 中断(OTG_FS_GINTSTS);
4> 在接收到 USB 数据包的个数后(PKTCNT),USB 核将内部自动将这个 OUT 端点的 NAK 为置 1,以
阻止接收更多数据包;
5> 应用程序处理 RXFLVL 中断和从 RXFIFO 读取数据;
6> 当应用读取完所有数据(等于 XFRSIZ)后,USB 核将产生一个 XFRC 中断(OTG_FS_DOEPINTx);
7> 应用处理这个 OTG_FS_DOEPINTx 中断并通过 OTG_FS_DOEPINTx 的中断为 XFRC 来判断传输
完成;

从上面步骤中的第 4 步中可以看出,当 USB 核收到来自 Host 端的数据后会自动将 OUT 端点关闭,这
也就是为什么在接收函数中在接收下一次数据时要再次使能这个 OUT 端点的原因。因此我们大体可以
判断出在 OUT 数据传输的过程中,USB 核会禁止端点->打开端点->禁止端点…如此不断循环中;那么
问题到底出现在哪里呢?会不会在 USB 核自动关闭端点后就没有再次成功打开?带着这样的怀疑心态
逐句查看代码,最终在接收函数的子函数中发现这么一段代码:

HAL_StatusTypeDef HAL_PCD_EP_Receive(PCD_HandleTypeDef *hpcd, uint8_t ep_addr, uint8_t*pBuf, uint32_t len)
{
    USB_OTG_EPTypeDef *ep;

    ep = &hpcd->OUT_ep[ep_addr & 0x7FU];

    /*setup and start the Xfer */
    ep->xfer_buff = pBuf;
    ep->xfer_len = len;
    ep->xfer_count = 0U;
    ep->is_in = 0U;
    ep->num = ep_addr & 0x7FU;

    if (hpcd->Init.dma_enable == 1U)
    {
      ep->dma_addr = (uint32_t)pBuf;
    }

   __HAL_LOCK(hpcd);
  
    if ((ep_addr & 0x7FU) == 0U)
    {
       USB_EP0StartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
    }
    else
    {
       USB_EPStartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
    }
    __HAL_UNLOCK(hpcd); 
    return HAL_OK;
}

之所以会怀疑这里,这是客户提供了一个信息,单向通信的时候就不会有问题!这是因为在发送数据
时,发送函数的底层函数内也使用到了这个互斥锁:

CDC_Transmit_FS() -> USBD_CDC_TransmitPacket() -> USBD_LL_Transmit() ->
HAL_PCD_EP_Transmit() :
HAL_StatusTypeDef HAL_PCD_EP_Transmit(PCD_HandleTypeDef *hpcd, uint8_t ep_addr, uint8_t*pBuf, uint32_t len)
{
    USB_OTG_EPTypeDef *ep;

    ep = &hpcd->IN_ep[ep_addr & 0x7FU];
    /*setup and start the Xfer */
    ep->xfer_buff = pBuf;
    ep->xfer_len = len;
    ep->xfer_count = 0U;
    ep->is_in = 1U;
    ep->num = ep_addr & 0x7FU;

 if (hpcd->Init.dma_enable == 1U)
 {
    ep->dma_addr = (uint32_t)pBuf;
 }

 __HAL_LOCK(hpcd);

 if ((ep_addr & 0x7FU) == 0U)
 {
    USB_EP0StartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
 }
 else
 {
    USB_EPStartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
 }

 __HAL_UNLOCK(hpcd);
 return HAL_OK;
}

接收处理数据时,底层是通过接收中断回调上来的,但发送时,我们往往将发送放到 main 等用户函数
中。这两个是不一样的,一个在中断内,一个在中断外,优先级别是不一样的,优先级不一样就有可
能导致资源冲突;

我们进一步查看__HAL_LOCK()宏定义:

 #define __HAL_LOCK(__HANDLE__)                                          \
                       do{                                                                           \
                               if((__HANDLE__)->Lock == HAL_LOCKED)  \
                               {                                                                       \
                                   return HAL_BUSY;                                      \
                               }                                                                       \
                               else                                                                  \
                               {                                                                       \
                                   (__HANDLE__)->Lock = HAL_LOCKED;    \
                               }                                                                        \
                           }while (0U)

若__HAL_LOCK(hpcd);失败则直接返回 return HAL_BUSY 的。为了验证在接收过程中是否
__HAL_LOCK 失败,我们引进全局变量 Lock_Flag,在发送函数中若成功 LOCK 则设置
Lock_Flag=1,UNLOCK 后则复位为 0:

uint8_t Lock_Flag =0;
HAL_StatusTypeDef HAL_PCD_EP_Transmit(PCD_HandleTypeDef *hpcd, uint8_t ep_addr, uint8_t*pBuf, uint32_t len)
{
    USB_OTG_EPTypeDef *ep;

    ep = &hpcd->IN_ep[ep_addr & 0x7FU];

    /*setup and start the Xfer */
    ep->xfer_buff = pBuf;
    ep->xfer_len = len;
    ep->xfer_count = 0U;
    ep->is_in = 1U;
    ep->num = ep_addr & 0x7FU;

    if (hpcd->Init.dma_enable == 1U)
    {
       ep->dma_addr = (uint32_t)pBuf;
    }

    __HAL_LOCK(hpcd);
   Lock_Flag =1;

    if ((ep_addr & 0x7FU) == 0U)
    {
       USB_EP0StartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
    }
    else
    {
       USB_EPStartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
    }

    __HAL_UNLOCK(hpcd);
    Lock_Flag =0;
    return HAL_OK;
}

接下来在接收函数中对全局变量 Lock_Flag 值进行判断,若为 1 则锁死程序,因为在 Lock_Flag=1 时,
则表示发送函数中已经获取了锁没有释放,此时若再去获取则会导致失败从而返回 HAL_BUSY;这里通
过锁死代码以便判断这种情况:

HAL_StatusTypeDef HAL_PCD_EP_Receive(PCD_HandleTypeDef *hpcd, uint8_t ep_addr, uint8_t*pBuf, uint32_t len)
{
    USB_OTG_EPTypeDef *ep;

    ep = &hpcd->OUT_ep[ep_addr & 0x7FU];

    /*setup and start the Xfer */
    ep->xfer_buff = pBuf;
    ep->xfer_len = len;
    ep->xfer_count = 0U;
    ep->is_in = 0U;
    ep->num = ep_addr & 0x7FU;

    if (hpcd->Init.dma_enable == 1U)
    {
       ep->dma_addr = (uint32_t)pBuf;
    }

   if(Lock_Flag ==1)
   {
      while(1);
   }
    __HAL_LOCK(hpcd);

    if ((ep_addr & 0x7FU) == 0U)
    {
       USB_EP0StartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
    }
    else
    {
       USB_EPStartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
    }
    __HAL_UNLOCK(hpcd);

    return HAL_OK;
}

通过调试,当出现问题时,程序果然被锁死在这个 while(1)了,这也证明了正是这个互斥锁所致。因此,
我们大体可以判断出现问题时流程大致如下:

1> 在 mian 函数中发送数据 CDC_Transmit_FS()
2> USBD_CDC_TransmitPacket()
3> USBD_LL_Transmit()
4> HAL_PCD_EP_Transmit()
5> __HAL_LOCK(hpcd); 此时成功获取互斥锁
6> 恰好此时有一个接收中断,由于 USB 中断具有优先级,跳转到接收中断内执行;同时,USB 核会
自动关闭 OUT 端点;
7> HAL_PCD_DataOutStageCallback()
8> USBD_CDC_DataOut()
9> CDC_Receive_FS()
10> USBD_CDC_ReceivePacket()
11> USBD_LL_PrepareReceive()
12> HAL_PCD_EP_Receive()
13> __HAL_LOCK(hpcd); 此时获取互斥锁失败导致返回,接收函数在 OUT 端点没有再次打开就已经提前结束,导致接收循环无以为继。

3 解决方案

知道了问题原因所在,接下来解决问题就相对来说比较容易的了。由于此问题是发送与接收处于不同优先等级导致资源冲突所致,那么我们可以将发送也放到与 USB 接收中断相同的中断等级中去,例如可以利用 USB 的 EOPF 中断,在开启 EOPF 中断后,在此中断内发送数据,这样发送与接收中断就处于相同等级了,EOPF 每 1ms 触发一次,速度完全可以。当然开启一个相同优先级的定时器来做发送数据也是可以,只不过定时器间隔得控制好。

此外,其实此问题是出现在 Cube 库的低版本中,例如 CubeF4 V1.5.0 和 CubeF2 V1.3.0 中都存在,但是在最新本的 CubeF4 V1.16.0,CubeF2 V1.6.0 版本中此问题得到了解决;此问题虽然后来发现是版本太旧所致,但从多个客户反馈此问题来看,此问题依然不失为一个很好的参考和教训。

来源: 21ic.com

围观 16
28

一、

STM32的AD转换,可以将转换任务组织为两个组:规则组和注入组。

在任意多个通道上以任意顺序进行的一系列转换构成成组转换。

例如,可以如下顺序完成转换:通道3、通道8、通道2、通道2、通道0、通道2、通道2、通道15。在执行规则通道组扫描转换时,如有例外处理则可启用注入通道组的转换。可以模糊的将注入组的转换理解为AD转换的中断一样,规则通道组的转换是普通转换,然而注入组的转换条件满足的情况下,注入组的转换会打断规则组的转换。

如果规则转换已经在运行,为了在注入转换后确保同步,所有的ADC(主和从)的规则转换被停止,并在注入转换结束时同步恢复。规则转换和注入转换均有外部触发选项,规则通道转换期间有DMA请求产生,而注入转换则无DMA请求,需要用查询或中断的方式保存转换的数据。

二、

规则组:此模式通过设置ADC_CR1寄存器上的DISCEN位激活。它可以用来执行一个短序列的n次转换(n

一个外部触发信号可以启动ADC_SQRx寄存器中描述的下一轮n次转换,直到此序列所有的转换完成为止。总的序列长度由ADC_SQR1寄存器的L[3:0]定义。

举例: n=3,被转换的通道 = 0、1、2、3、6、7、9、10 第一次触发:转换的序列为 0、1、2 第二次触发:转换的序列为 3、6、7 第三次触发:转换的序列为 9、10,并产生EOC事件 第四次触发:转换的序列 0、1、2,依次类推;

注入组:此模式通过设置ADC_CR1寄存器的JDISCEN位激活。在一个外部触发事件后,该模式按通道顺序逐个转换ADC_JSQR寄存器中选择的序列。

一个外部触发信号可以启动ADC_JSQR寄存器选择的下一个通道序列的转换,直到序列中所有的转换完成为止。总的序列长度由ADC_JSQR寄存器的JL[1:0]位定义。

例子: n=1,被转换的通道 = 1、2、3 第一次触发:通道1被转换 第二次触发:通道2被转换 第三次触发:通道3被转换,并且产生EOC和JEOC事件 第四次触发:通道1被转换,也是依次类推。

三、

STM32的ADC的工作模式:

单次转换模式:转换一次则停止;
连续转换模式:转换完一次后即开始下一次转换;
扫描模式:扫描一组模拟通道;
间断模式:每触发一次,转换序列中n个通道。

四、

因为规则通道转换的值储存在一个仅有的数据寄存器中,所以当转换多个规则通道时需要使用DMA,这可以避免丢失已经存储在ADC_DR寄存器中的数据。只有在规则通道的转换结束时才产生DMA请求,并将转换的数据从ADC_DR寄存器传输到用户指定的目的地址。

注: 只有ADC1和ADC3拥有DMA功能。由ADC2转化的数据可以通过双ADC模式,利用ADC1的DMA功能传输。

五、

双ADC模式

(1)同步注入模式:此模式转换一个注入通道组。外部触发来自ADC1的注入组多路开关(由ADC1_CR2寄存器的JEXTSEL[2:0]选择),它同时给ADC2提供同步触发。

注意: 不要在2个ADC上转换相同的通道(两个ADC在同一个通道上的采样时间不能重叠)。

(2)同步规则模式:此模式在规则通道组上执行。外部触发来自ADC1的规则组多路开关(由ADC1_CR2寄存器的EXTSEL[2:0]选择),它同时给ADC2提供同步触发。

(3)快速交叉模式:此模式只适用于规则通道组(通常为一个通道)。外部触发来自ADC1的规则通道多路开关。

外部触发产生后:
A.ADC2立即启动并且
B.ADC1在延迟7个ADC时钟周期后启动

注意:最大允许采样时间

(4)慢速交叉模式:此模式只适用于规则通道组(只能为一个通道)。外部触发来自ADC1的规则通道多路开关。

外部触发产生后:
A.ADC2立即启动并且
B.ADC1在延迟14个ADC时钟周期后启动
C.在延迟第二次14个ADC周期后ADC2再次启动,如此循环。

(5)交替触发模式:此模式只适用于注入通道组。

外部触发源来自ADC1的注入通道多路开关。
A.当第一个触发产生时,ADC1上的所有注入组通道被转换。
B.当第二个触发到达时,ADC2上的所有注入组通道被转换。
C.如此循环……

(6)独立模式:此模式里,双ADC同步不工作,每个ADC接口独立工作。

(7)混合的规则/注入同步模式:规则组同步转换可以被中断,以启动注入组的同步转换。

(8)混合的同步规则+交替触发模式:规则组同步转换可以被中断,以启动注入组交替触发转换。 显示了一个规则同步转换被交替触发所中断。

(9)混合同步注入+ 交叉模式:一个注入事件可以中断一个交叉转换。这种情况下,交叉转换被中断,注入转换被启动,在注入序列转换结束时,交叉转换被恢复。

来源: eeworld

围观 6
204

STM32 防火墙(Firewall)能够构建一个与其它代码隔离的带有数据存储的可信任代码区域,结合 RDP、WRP 以及 PCROP,可用来保护安全敏感的算法。在 STM32 Cube 固件库参考代码里提供了几个不同的防火墙配置。那么问题来了,什么是STM32 防火墙的应该使用的安全配置呢?本文以 STM32 参考手册为基础,以最大化安全为目标,来探索发现 STM32 防火墙的推荐配置。

STM32 防火墙介绍

STM32 防火墙保护特定代码/数据不被保护区域之外的执行所访问。代码和数据位于 Flash 存储器中,也可以位于 SRAM1 中。

可选择配置的防火墙的三个保护段如下:

 代码段(Flash)
 数据段(Flash)
 易失数据段(SRAM1), 可被配置为可执行

防火墙配置激活后,对受保护代码的访问必须唯一的通过调用门(Call gate)进行。防火墙外设监听 AMBA 总线,任何不通过调用门的访问,将导致系统重启。

发现 STM32 防火墙的安全配置
Figure 1 防火墙的连接

不同于 STM32 上的 ARM MPU 技术,防火墙激活后,将一直保持激活状态,直至下次系统复位。

防火墙的调用门

防火墙的调用门由三个字组成,位于 Flash 中的代码段以及配置成非共享且可执行的 SRAM1 数据段,开始地址的前三个 32位。

 第 1 字: 虚设。总是处于关闭状态。用于保护指令预取造成的对调用门的访问。
 第 2 和第 3 字:总是处于打开状态。

为了打开防火墙,代码必须跳到调用门的第 2 字执行,且第 2 字和第 3 字的执行不能被中断,否则会导致系统重启。

防火墙的状态图

防火强的配置激活后,处于关闭(Close)状态。在关闭状态下,对受保护区域的访问将被禁止。跳到防火墙的调用门处执行,则防火墙打开(open)。在防火墙打开状态下,若防火墙控制寄存器(FW_CR)的 Prearm(FPA)位依然为 0,跳转至非保护代码将导致系统重启。将防火墙控制寄存器的 Prearm 位设置成 1,这时任何对非保护代码的访问将导致防火墙进入关闭(close)但激活状态。

发现 STM32 防火墙的安全配置
Figure 2 防火墙的状态转换

STM32 防火墙例程

一个 STM32 防火墙的实例可在 CubeMX 固件包里找到。例如:

STM32Cube\Repository\STM32Cube_FW_L4_V1.6.0\Projects\STM32L476RG-Nucleo\Examples\FIREWALL

工程文件支持 IAR 和 Keil 开发环境,含有两个目录:

FIREWALL_VolatileData_Executable
FIREWALL_VolatileData_Shared

读者可使用 IAR 或者 Keil 打开这两个例子进行编译以及运行。

例程防火墙配置引出的疑问

例程在激活防火墙时使用了不同的配置,演示了防火墙外设的灵活性与不同的安全性,具有很好的学习用途,但是在实际应用中,为最大化安全考虑,我们究竟应该去应用哪一种配置 呢?

 配置一: 仅配置保护 SRAM1 中的数据,且将 SRAM1 的数据配置成可执行且非共享。注意没有对 Flash 的任何地方配置保护。

 /* No protected code segment (length set to 0) */
 fw_init.CodeSegmentStartAddress = 0x0;
 fw_init.CodeSegmentLength = 0;

 /* No protected non-volatile data segment (length set to 0) */
 fw_init.NonVDataSegmentStartAddress = 0x0;
 fw_init.NonVDataSegmentLength = 0;

 /* Protected volatile data segment (in SRAM1 memory) start address and length */
 fw_init.VDataSegmentStartAddress = 0x2000F100;
 fw_init.VDataSegmentLength = 3840; /* 0xF00 bytes */

 /* The protected volatile data segment can be executed */
 fw_init.VolatileDataExecution = FIREWALL_VOLATILEDATA_EXECUTABLE;

 /* The protected volatile data segment is not shared with non-protected
 application code */
 fw_init.VolatileDataShared = FIREWALL_VOLATILEDATA_NOT_SHARED;

 配置二: 保护 Flash 里的代码和数据,以及 SRAM1 中的数据。SRAM1 中的数据被配置为不可执行,但可共享

 /* Protected code segment start address and length */
 fw_init.CodeSegmentStartAddress = 0x08010000;
 fw_init.CodeSegmentLength = 512; /* 0x200 bytes */

 /* Protected non-volatile data segment (in FLASH memory) start address and length */
 fw_init.NonVDataSegmentStartAddress = 0x080FF000;
 fw_init.NonVDataSegmentLength = 256; /* 0x100 bytes */

 /* Protected volatile data segment (in SRAM1 memory) start address and length */
 fw_init.VDataSegmentStartAddress = 0x20000000;
 fw_init.VDataSegmentLength = 576; /* 0x240 bytes */

 /* The protected volatile data segment can't be executed */
 fw_init.VolatileDataExecution = FIREWALL_VOLATILEDATA_NOT_EXECUTABLE;

 /* The protected volatile data segment is shared with non-protected
 application code */
 fw_init.VolatileDataShared = FIREWALL_VOLATILEDATA_SHARED;

防火墙配置分析

SRAM1 的共享 Vs. 非共享

用户手册 RM0351 中提到若一段 SRAM1 被防火墙设置为共享(VDS=1),则该区域总是可被访问,而不用去管防火墙是打开,关闭还是没有激活。 同时,SRAM1 中的数据可被执行, 而不需要激活防火墙更不需要打开防火墙。换言之,共享区域其实是不在防火墙的保护之下。因此,为保护 SRAM1 中的敏感数据,我们应该避免使用共享配置,也就是 VDS 应当是 0。

发现 STM32 防火墙的安全配置
Figure 3 RM0351 中关于防火强的易失数据段配置成可共享

SRAM1 中的执行 Vs. 非执行

用户手册 RM0351 中提到,若 SRAM1 被配置成非共享(VDS=0)且可执行(VDE=1), 防火墙的调用门序列需要先被执行。这里有几层含义,一是这段代码已经处于防火墙的保护之下;二是这段代码可被调用门中的代码调用;三是这段代码可以是调用门(参考调用门的描述)。前面我们提到防火墙打开后,保护区域之内的执行可以访问保护区域之内的代码以及数据。换言之, 这里的 SRAM1 执行的代码是可以访问 Flash 里受防火墙保护的代码和数据。SRAM1 中的代码是可变 的,为避免不确定性,一般情况下我们应当避免 SRAM1 运行的代码访问 受保护的 Flash 代码和数据,也就是建议将 SRAM1 中配置成非执行。

发现 STM32 防火墙的安全配置
Figure 4 RM0351 中关于防火强的易失数据段配置成可执行

是否应该配置 Flash 的数据段(Non-volatile data)

前面的例程配置一仅配置了 SRAM1。然而用户手册 RM0351 中提到,Flash 里的数据段未被定义,则 FW_CR 寄存器可在任意时刻进行修改,而不用管 防火墙是打开还是关闭。

发现 STM32 防火墙的安全配置
Figure 5 RM0351 中关于防火强控制寄存器的动态配置方式的描述

也就是说,防火墙相比较 MPU 的优势“配置有效直到系统复位”,在这种配置下不再存在了。例如在例程“配置一”里,SRAM1 的代码配置成可执行非共享,受防火墙保护。然而在防火墙设置好且处于关闭状态后,我们依然可以通过保护区域之外这样的代码将该保护去掉,例如将它配置后再次改成可执行且共享。

__HAL_FIREWALL_VOLATILEDATA_SHARED_ENABLE();

读者可轻易将该代码加入到 FIREWALL_VolatileData_Executable 进行试验。这显然不是我们想要的安全配置, 我们应该避免这种用法。 所以,我们总是配置防火墙保护 Flash 中的某一数据段,使得 NVDSL 寄存器不为 0。

推荐的防火墙配置

综合前面的例子和分析,为最大化安全考虑,我们推荐使用防火墙 时使用以下典型配置。它配置了三个保护段,Flash 中的代码和数据,SRAM1 中数据;SRAM1 中数据配置成不可共享且不可执行。同时要注意修改编译器链接文件将三块受保护的代码和数据分别放置到相应的下列定义的区域(Region)里。链接文件, 对 IAR 是.icf 文件,对 MDK 是.sct 文件。

 /* Protected code segment start address and length */
 fw_init.CodeSegmentStartAddress = FW_CODE_START_ADDRESS;
 fw_init.CodeSegmentLength = FW_CODE_LENGTH;

 /* Protected non-volatile data segment (in FLASH memory) start address and length */
 fw_init.NonVDataSegmentStartAddress = FW_DATA_START_ADDRESS;
 fw_init.NonVDataSegmentLength = FW_DATA_LENGTH;
 /* Protected volatile data segment (in SRAM1 memory) start address and length */
 fw_init.VDataSegmentStartAddress = FW_VDATA_START_ADDRESS;
 fw_init.VDataSegmentLength = FW_VDATA_LENGTH;
 /* The protected volatile data segment can't be executed */
 fw_init.VolatileDataExecution = FIREWALL_VOLATILEDATA_NOT_EXECUTABLE;

 /* The protected volatile data segment is not shared with non-protected
 application code */
 fw_init.VolatileDataShared = FIREWALL_VOLATILEDATA_NOT_SHARED; 

因为控制寄存器 FW_CR,包括 Prearm 位、共享执行、设置,只能在防火墙打开时才能修改,因此在推荐配置下,典型的调用门代码执行序列应如下:

 清除 Prearm 位 (FPA)
 执行安全算法
 清除所有中间数据
 清除 CPU 寄存器信息
 设置 Prearm 位 (FPA)
 退出保护区域

结论

本文根据 STM32 参考手册, 提出了一个 STM32 防火墙的安全配置,可在实际 STM32 防火墙的案例中直接应用。也可以利用本文加深对 STM32 防火墙功能的理解。

来源: 21ic.com

围观 5
362

前言

STM32 提供了丰富的音频应用外设,并得益于灵活高效的内部架构,可以支持广泛的音频应用。本文中,在简单介绍音频采集的背景知识后,从应用需求出发,确定麦克风的选用。然后,描述了 STM32 内部 DFSDM (Digital Filter for SigmaDelta Modulator)在 PDM 麦克风采集中应用。最后逐步介绍如何利用 STM32CubeMX 进行 DFSDM 设计开发,实现 PDM麦克风声音采集。

一 背景知识

声音通过声学传感器获取模拟信号,经过模数转换器,转换成二进制码 0 和 1,这些 0 和 1 便构成了音频数字信号。

PDM 麦克风能够实现上述的模拟信号获取,并输出 PDM 信号。PDM(Pulse Density Modulation)脉冲密度调制,利用脉冲密度表示模拟信号强度。

从 PDM 位流中获取数据,还需要经过如下图环节才能获得模拟信号幅度对应的数字量。

利用 DFSDM 开发 PDM 麦克风应用介绍

二 应用需求及 DFSDM 支持分析

在音频应用开发前,需要根据应用需求,对麦克风个数、支持编码类型、采样率及分辨率等进行确定。下面围绕 DFSDM在这些需求方面的支持情况进行分析。

2.1 麦克风数量

同时运行的最大麦克风数量,对于 DFSDM,由 DFSDM 中滤波器数量决定。简单理解,就是一个滤波器对应一个麦克风。注意这种简单等同并不适用于非同时采样的应用场景。

麦克风的数量不直接对应通道数量,如下图。

利用 DFSDM 开发 PDM 麦克风应用介绍

可映射任一 DFSDM 通道单元至滤波器单元。对于通道 CH(y-1),数据线可来源于 DATIN(y-1)引脚,也可来自于DATIN(y),在通道单元中可以选择获取数据的时刻(上升沿或者下降沿)。这样带来的益处是,可利用内部两个通道单元对同一个数据线上数据进行分离并处理获得采样数据。而这个应用,直接满足了双通道数据在同一条数据线上的数据采集场景。

上述描述的应用场景中,数据处理流向如下图所示:

利用 DFSDM 开发 PDM 麦克风应用介绍

注 1:图中 CLK 线硬件设计上不一定需要连接,DFSDM 可在内部关联,实现利用输出时钟作为时钟输入,具体可通过参考手册了解。

2.2 编码类型

DFSDM 支持 PDM、曼彻斯特编码,支持具有类似编码的麦克风器件,具体可通过参考手册了解。

2.3 采样率

DFSDM 通过时钟源、滤波模式、快速模式选择、过采样配置,实现不同采样率的支持。能够支持常见的8k,16k,22k,44k,48k 的采样率,也能够支持特殊应用场合所需的更高采样率,例如 192k, 384k 等。
更多关于 DFSDM 采样率介绍,以及具体计算公式可通过参考手册了解。

2.4 分辨率

DFSDM 具有 24 位数据寄存器,可通过配置实现不同分辨率的支持,有效数据最高支持到 24 位。同时,新的 HAL 库支持全硬件获取 16 位采样数据,不增加 CPU 负载。

DFSDM 分辨率由过采样率,滤波器类型和右移位器决定,更多内容可通过参考手册了解。

利用 DFSDM 开发 PDM 麦克风应用介绍

在不同处理环节数据分辨率情况如下图所示。

利用 DFSDM 开发 PDM 麦克风应用介绍

积分器最大数据输出范围如下表。例如当 FOSR 为 64,IOSR 为 1,采用 Sinc3滤波,在不考虑后续处理环节时,输出范围为±262144,分辨率能够达到 19 位。并可通过右移位器灵活获得需要的有效数据位数。

利用 DFSDM 开发 PDM 麦克风应用介绍

三 前期准备

出于将重心放在 DFSDM 应用介绍,简化其他环节考虑。本文中实现例在 ST 提供的 NUCLEO-L476RG 和 X-NUCLEOCCA02M1板展开。

考虑到利用 DFSDM 实现 PDM 麦克风采集,首先根据 UM1900 对麦克风板 X-NUCLEO-CCA02M1 进行处理,使其支持基于 DFSDM 采集的两路 PDM 麦克风。需要准备软硬件资源如下表。

利用 DFSDM 开发 PDM 麦克风应用介绍

四 实现过程

4.1 应用实现

利用 X-NUCLEO-CCA02M1 板上已有的两路 PDM 麦克风,可实现最多两路麦克风数据采集。

在本例中,先将应用需求定为两路麦克风采集,采样率为 8KHz,分辨率为 16-bit。后续介绍如何利用 STM32CubeMX生成遵循应用需求的工程,以及在获得工程后,如何启动采样,实现麦克风采集的应用。

4.2 开发流程

利用 DFSDM 开发 PDM 麦克风应用介绍

4.3 STM32CubeMX 配置实现

STM32CubeMX 操作流程如下图所示。

利用 DFSDM 开发 PDM 麦克风应用介绍

a. DFSDM 通道选择

根据 X-NUCLEO- CCA02M1 板原理图,可知在将其处理成支持 DFSDM 采集的两路麦克风时,麦克风总线引脚与STM32L476RG 连接情况如下。

利用 DFSDM 开发 PDM 麦克风应用介绍

在 STM32CubeMX 中,选择 Channel1 中“ PDM/SPI Input from ch2 and internal clock ”和 Channel2 中“ PDM/SPI Inputfrom ch2 and internal clock ”,并选择“CKOUT”,如下图所示,PC2、PB14 自动对应与 DFSDM 的 Clock out 和 Data In功能脚。

利用 DFSDM 开发 PDM 麦克风应用介绍

b. 配置通道

切换至“Configuration” 标签页,点击“Control\DFSDM”打开 DFSDM 配置界面。由于选择了通道 1 和通道 2,这里可以对这两个通道进行配置。配置情况如下图。

利用 DFSDM 开发 PDM 麦克风应用介绍

Type:配置数据读取时刻。SPI with rising edge 在时钟的上升沿读取数据;SPI with falling edge 在时钟下降沿读取数据。

Spi clock:总线时钟源。Internal SPI clock 利用内部时钟,对应为 CKOUT 时钟。
Offset:数据偏移量补偿。

Right Bit Shift : 右移位。右移位的确定,涉及到获取有效数据的位数,如“分辨率”小节中图所示。需要结合滤波器和积分器配置及分辨率需求进行确定。本文中,经过滤波器和积分器处理后输出数据分辨率为 29-bit,所以将右移位设置为 5,从而在 24 位数据寄存器中获得有效的 24-bit 数据。

Analog watchdog parameters : DFSDM 中模拟看门狗参数设置。应用例中没涉及到此功能,参数保持默认。

c. 配置滤波器

切换至“Filter0”/ “Filter1” 标签页,配置情况如下图。

利用 DFSDM 开发 PDM 麦克风应用介绍

Regular channel selection: 数据来源。选择与外部麦克风连接的通道。

Continuous mode: 转换模式。选择连续转换模式。

Trigger to start regular conversion: 启动转换的触发源。

Fast mode: 快速模式。在连续转换数据源来自于同一个通道时可启用,能够提高转换速度。本文应用例满足条件,使能Fast mode。

DMA mode : DMA 模式。如果无法使能,需参考后面“配置 DMA”小节,先完成对 DMA 参数的配置。

Inject channel selection: 注入通道选择。应用例中没涉及到此功能,参数保持默认。

Sinc Order: 滤波类型。

Fosr: 滤波器过采样率。

Iosr: 积分器过采样率。

其中,Sinc order, Fosr 和 Iosr 共同决定了积分器处理后的内部数据分辨率。根据“分辨率”小节,在如上配置时,积分器最大输出分辨率为 29 位。

d. 配置时钟

切换至“Output Clock” 标签页,配置情况如下图。

利用 DFSDM 开发 PDM 麦克风应用介绍

Selection: CKOUT 时钟源选择。

Divider: 时钟预分频因子。CKOUT = Audio clock / divider。

其中,Audio clock 来源于 SAI1 时钟,如下图配置,SAI1 时钟配置为 17.411765MHz。CKOUT 输出时钟频率为1.0242MHz。

在 Fast mode 情况下,采样率 Fs 如下:

利用 DFSDM 开发 PDM 麦克风应用介绍

利用 DFSDM 开发 PDM 麦克风应用介绍

e. 配置 DMA

切换至“DMA Settings” 标签页,配置情况如下图。

利用 DFSDM 开发 PDM 麦克风应用介绍

DMA 配置中,选择 Circular 模式,可实现循环向数据 buffer 中填充采样数据。
Data Width 设置为 Half Word,以便实现只获取数据寄存器的高 16 位数据,实现 16-bit 分辨率数据采集。

f. 生成工程

在 STM32CubeMX\Help 打开帮助文档 UM1718,参考文档,生成 IAR 工程。至此获得与硬件对应,支持两路麦克风,采样率为 8KHz,分辨率为 16-bit 的初始化软件工程。

g. 启动采样

完成上述步骤后,即可调用 HAL 库中提供的函数,启动麦克风数据采集。建议利用 DMA 方式实现麦克风采集,占用 CPU 开销最小。下述为启动采样的实现例程。在例程中通过调用 HAL_DFSDM_FilterRegularMsbStart_DMA 启动采样,这个函数实现了利用 DMA 接收 DFSDM 的数据寄存器的高 16 位数据,并传输到分配的 Buffer 空间中。

注:HAL_DFSDM_FilterRegularMsbStart_DMA()的实现需要在 DMA 配置中将 Data Width 设置为 Half Word。

由于 DMA 采用 Circular 模式,采样数据向 buffer 空间的搬运连续进行,而半传输完成和传输完成中断回调函数被执行时,意味新的数据被填充至 Buffer 空间,用户可以在其中增加处理指令,完成利用 DFSDM 的麦克风数据采集应用的开发。

#define SAMPLE_FREQ 8000
#define BYTE_PER_SAMPLE 2
#define MICROPHEN_NUMBER 2
#define FRAME_NUMBER 2
//16bit sample resolution
#define BUF_LENGTH (SAMPLE_FREQ/1000*MICROPHEN_NUMBER*FRAME_NUMBER)
…
/* Buffer 分配 */
int16_t Buf_Mic0[BUF_LENGTH];
int16_t Buf_Mic1[BUF_LENGTH];
…
main()
{
…
/*启动采样,在初始化完成后调用*/
HAL_DFSDM_FilterRegularMsbStart_DMA(&hdfsdm1_filter0,Buf_Mic0,BUF_LENGTH);
HAL_DFSDM_FilterRegularMsbStart_DMA(&hdfsdm1_filter1,Buf_Mic1,BUF_LENGTH);
…
While (1){}
…
}
…
/* I2S DMA 回调函数 */
void HAL_DFSDM_FilterRegConvHalfCpltCallback(DFSDM_Filter_HandleTypeDef *hdfsdm_filter)
{
 //半传输完成,可在此添加对采样数据的处理
}
void HAL_DFSDM_FilterRegConvCpltCallback(DFSDM_Filter_HandleTypeDef *hdfsdm_filter)
{
 //传输完成,可在此添加对采样数据的处理
}

除此之外,还可以调用 HAL_DFSDM_FilterRegularStart_DMA 启动采样,而这种实现获取的整个 32 位的数据寄存器内容,获取数据需要利用软件指令将低 8 位,非采样数据移除。同时这种实现,需要在 DMA 配置中,将 Data Width 配置为 Word。

五 小结

本文虽然尽可能详细的介绍在 PDM 麦克风采集中,DFSDM 作用及实现步骤。但并没有涉及到所有 DFSDM 支持功能的介绍,而是侧重于 DFSDM 在 PDM 麦克风采集中涉及的功能介绍,更多 DFSDM 功能介绍可以通过参考手册了解。另外不同系列 STM32 的 DFSDM 支持情况会略有差异,需以对应型号的参考手册为准。

来源:ST

围观 10
370

近日有客户反映,他在在使用STM32F103C8T6的时候遇到如下问题:

I2C1使用PB6和PB7口,定时器TIM3使用PB0\PB1\PB4\PB5做4路PWM。但在使用的过程中,如果只初始化定时器就没有任何问题,但是一旦初始化I2C1,那么定时器的通道2(PB5)就不能产生PWM波,而是保持高电平。

客户查阅手册得知PB5的默认复用功能是I2C1的SMBA引脚,但是它的I2C1是初始化为I2C模式的,并不是初始化为SMBAS模式,而且同样的方式在F0上测试是可用的。它本来用的是标准库开发的,然后尝试使用STM32CubeMx进行硬件配置,使用HAL库新建工程,还是存在同样的问题。

就上面的问题,查看了其有关I2C1和TIM3d的pwm初始化的部分代码,并未发现不对的地方。首先重点怀疑I2C1的配置是否有误,担心客户在配置I2C1时配置成了SMBAS模式。借助于库代码,进一步跟踪下去查看底层的寄存器配置,相关寄存器操作也没有发现问题。

这里TIM3的PWM输出的几个管脚有涉及到重映射【REMAP】,从数据手册的管脚分配上来看,如果不开启I2C1的SMBA模式,不应该存在冲突问题。

基于STM32F1系列的重要宝典RM0008参考手册

这边再次使用STM32CubeMx基于STM32F103C8进行同样配置,结果跟客户上面反馈的一样。不开启I2C1时,TIM3的所有管脚功能正常;开启I2C1后,TIM3的部分管脚PB5功能异常。感觉问题可能出在跟TIM3的remap这个地方。打开基于STM32F1系列的重要宝典---参考手册RM0008,查看核对有关TIM3的管脚复用REMAP功能介绍的地方。

基于STM32F1系列的重要宝典RM0008参考手册

现在客户执行的是TIM3的部分管脚重映射功能【partial remap】,从上面表格来看,目前的代码配置是没有问题的。毕竟目前如果不开启I2C1的话TIM3也没什么异常,所以过来查看这个地方,心里也没怎么期望从这里找出明显错误,倒是期待从附近能否找到些额外的提示或提醒。这不,表格的下方用了小一号文字明确提示:上述REMAP操作仅适用于64脚、100脚和144脚封装的芯片.现在客户用的芯片是STM32F103C8,管脚数为48,换言之,它是不支持TIM3的复用功能脚的REMAP操作的。到此,问题应该说找到原因了。

过不了几天,客户又发邮件过来继续就该问题咨询。他问,既然说48脚芯片STM32F1不支持TIM3的REMAP操作,那为什么做了REMAP操作后,如果不开启I2C1,TIM3的4个脚的PWM功能很正常;或者说即使同时开启了I2C1,PB4的功能还是正常REMAP过来了,只是PB5功能异常。希望我这边给出进一步解释。

站在用户的角度有人会发出类似疑问很正常。其实,既然手册明确规定48脚的STM32F1芯片不支持TIM3功能脚的REMAP,它自有其原因和道理。你违背手册之规定来操作,结果的正确性就不能得到保障。有时REMAP没问题,不代表任何时候进行REMAP没问题。就像讨论某个命题,局部、个别情形成立,并不能说它恒成立。打个形象的比方,A今年10岁,B今年20岁。即B比A大10岁,B今年的年龄是A的2倍。显然,两个结论站在今年都成立,到了明年,后面的2倍论就不成立了。

在ST MCU的应用过程中,还经常出现类似违背手册规定的操作以及由此导致的疑问。比方说,有人发现使用STM32芯片内部的flash时,似乎可以使用到手册规定以外的空间。用户这样使用,芯片的功能或特性是不能得到保障的,作为厂家只能保证芯片手册规定区域的品质。又比方,我们知道ST MCU绝大部分芯片都带有UID,可有些人发些即使手册明确没有UID的芯片,他们似乎发现这类芯片还是有UID甚至加以利用,询问这样是怎么回事或者说是否可靠。同样,对于类似情形作为厂家也只能保证手册规定的特性。超出手册规定以外的应用,只能用户自己负责。

好,继续回到上面的话题。

我们从芯片应用的参考手册上应该说找到了明确的规定或答案。我们还可以查看下基于该芯片有无更为详尽的勘误表。后来在官方网站找到了相应的勘误手册【注:勘误手册往往基于芯片型号,即一个系列可能有多个勘误手册】,我们在勘误手册里也看到关于上面问题的详细描述,可应视为对参考手册的进一步补充。

基于STM32F1系列的重要宝典RM0008参考手册

到此,问题原因基本明了。或许还会有人问,上面提到使用STM32CubeMx进行过工程配置,配置过程并未发现异常,或者说配置过程中没有遇到上面阻碍。既然参考手册规定不允许STM32F103C8芯片的TIM3 remap操作,在开启i2c1时,通过cubeMx配置TIM3的REMAP功能时应该出现非法提示才对啊?

我使用的CUBEMX的版本是4.22.0,在开启I2C1的同时,并按照TIM3的部分REMAP配置时不能说没有给出提醒,只能说提醒得不够明确。该提醒可能容易被人忽视,然后可以一路配置下去。

STM32CubeMx配置如下图,在I2C1那个地方有黄色警示,鼠标放过去的时候是有文字提示的【不一定每个人会留意到】:

基于STM32F1系列的重要宝典RM0008参考手册

可以说CubeMx还是有不够严谨或者说考虑不周的地方。如果在开启I2C1情况下,当用户试图配置PB5作为输出时直接红色警告拒绝TIM3的remap就好了。但这样,可能又会影响到另外一类用户人群,他们根本不在乎PB5怎样,只关注PB4能用作PWM输出就好。有点众口难调的味道,参考手册在明确不支持STM32F1系列48脚的TIM3的REMAP操作的同时,结合勘误手册做了应用补充,以尽可能满足不同的应用需求。

毕竟STM32CubeMX工程浩大,肯定还有需要完善的地方,尤其类似的细节问题。不过,我们相信会越来越完善。不管怎样,所以,任何时候我们不能完全将芯片手册丢在一边。比如,我们知道ST官方出了基于各个STM32系列的固件库,库里各类示例工程极大方便了大家的学习和研发。不难想象,这些固件库工程也都比较庞大,难免会有bug,一直都处于不断完善中。在使用它们的过程中如果碰到疑惑的地方,不妨查看下相关数据手册或开发参考手册,做进一步比对确认。如果觉得手册还描述得不够清晰明确的话,可以去找找相应芯片的勘误手册,看看里面有无相关问题的进一步补充描述。

唠叨一堆,抛砖引玉。

转自: STM32单片机

围观 9
358

前言

某客户在调试 STM32L053 的比较器 1 时,使用内部 1.2V 的参考电压,没有问题.但当使用比较器 2 时,使用同样的设置,却发现比较电压无法调到 1.2V,只能设置到 0.6V 左右,到时是什么问题呢?

问题解决

问题调试

首先得到这个问题,我们先比较一下两种现象之间的设置问题,发现比较器 1 和比较器 2 的设置都是一样的.然后我们通过修改比较器 2 的内部比较电压查看现象,发现就算我们设置为二分之一的内部参考电压(二分之一的 1.2V),触发门限依旧是 0.6V.即使使用 cubeMx 重新生成代码,现象也没有得到解决.

解决方法

然后我们开始查看参考手册,我们猜想,两个比较器之间是否有不一样的设置,以为比较器 2 更为高级,有更多的设置.排除功耗和速度的不一样设置外.我们通过搜寻 comp2 发现其在 Reference control and status register (SYSCFG_CFGR3)里面有个不一样的设置, Bit 0 EN_VREFINT.具体描述如下 :

STM32L053 comp2 比较电压无效问题

如描述,我们在某些模式下,必须设置这一位.所以我们做以下修改.在使能比较器 2 之前先设置这一位.

SET_BIT (SYSCFG- > CFGR3, 1);
HAL_COMP_Start(&hcomp2);

在这修改后,比较器 2 的比较电压可以得到正常的电压值,如 1.2V.

总结

在同一个类型外设中,如果两个外设同样的设置,却得不到同样的效果.我们可以通过查询参考手册对于这两个外设的不同描述,根据不同的描述,找出与现象之间有相关的差异,进行修改与调试.往往可以得到比较好的效果,或者提示.

来源: http://stmcu.com.cn/

围观 5
328

页面

订阅 RSS - STM32