本笔记主要记录在学习《安富莱 STM32-V7 开发板用户手册》需要注意的点。使用的开发板为反客的同型号开发板
如何配置下载算法
什么是下载算法
简单来说就是一段在 SRAM 运行的程序,将对应的hex文件烧录到指定的外部flash中。
现在我的程序分为两部分bootloader(内部flash),app(外部flash)。当你需要使用外部flash存放里的app时需要先下载bootloader程序到内部flash里。
一个简单的 bootloader 程序的样子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
QSPI_HandleTypeDef hqspi ;
UART_HandleTypeDef huart1 ;
void SystemClock_Config ( void );
static void MX_GPIO_Init ( void );
static void MX_USART1_UART_Init ( void );
static void MX_QUADSPI_Init ( void );
typedef void ( * pFunction )( void );
int main ( void )
{
SCB_EnableICache (); // 使能ICache
SCB_EnableDCache (); // 使能DCache
HAL_Init ();
SystemClock_Config ();
MX_GPIO_Init ();
MX_USART1_UART_Init ();
MX_QUADSPI_Init ();
QSPI_W25Qxx_Init ();
QSPI_W25Qxx_MemoryMappedMode (); // 配置QSPI为内存映射模式
SCB_DisableICache (); // 关闭ICache
SCB_DisableDCache (); // 关闭Dcache
SysTick -> CTRL = 0 ; // 关闭SysTick
SysTick -> LOAD = 0 ; // 清零重载值
SysTick -> VAL = 0 ; // 清零计数值
JumpToApplication = ( pFunction ) ( * ( __IO uint32_t * ) ( W25Qxx_Mem_Addr + 4 )); // 设置起始地址
__set_MSP ( * ( __IO uint32_t * ) W25Qxx_Mem_Addr ); // 设置主堆栈指针
JumpToApplication (); // 执行跳转
while ( 1 );
}
keil
添加下载算法到keil路径(.flm文件)
找到商家提供的.flm文件,复制到keil存放路径的上一页,放到keil-ARM-Flash文件夹即可
魔术棒-Debug-Setting-FlashDownload-Add-选择上面加入的算法(这样keil就知道这段地址是存在的)
魔术棒-Target-Read/Only Memory Areas下将第一个勾选,配置为ROM1:Start:0x90000000Size:0x2000000(注意这里是32MB对应的),其他的选项都叉掉
这步在app中完成
程序需要添加一个
1
SCB -> VTOR = 0x90000000 ; // 重定向向量表
注意
如果后面无法加载下载算法,可能有一下原因:
线没接好
下载时钟过快
在Flash Download 页面中的 RAM for Algorithm 里的 Size 调大点
rtt studio
stm32prog(.stldr文件)
配置工程-下载-勾选外部下载器-选择指定的算法文件-添加商家提供的.stldr文件
工程目录-linkScript-…-link.lds
将ROM修改为0x90000000,大小改为32768(这里是因为我的开发板上有一块 32MB 的 QSPI FLASH,这里需要按照自己的开发板大小进行填写)
jlink
我手上没有jlink,这里引用别人的文章
参考:
“使用 ART-Pi(STM32H750) 片外8M 之JLINK”
如何编写下载算法
flm文件
注意flash写操作尽量以256bits为单位进行(每256bits配10bit的ECC位,可以检测1个bit并纠正或是检测2个bit),这样硬件ECC不容易出问题。flash读操作支持64bits,32bits,16bits和8bits
找到…\keil\ARM\Flash_Template
“参考”
stldr文件
“参考”
Chap.9 EventRecoder(keil独有,通用调试器)
pre
基于EventRecoder的printf重定向
需要启用MicroLib
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "EventRecorder.h"
int main ( void )
{
/* USER CODE BEGIN 1 */
//放最前面
EventRecorderInitialize ( EventRecordAll , 1U );
EventRecorderStart ();
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config ();
while ( 1 )
{
printf ( "hello world from SWD" );
HAL_Delay ( 1000 );
}
/* USER CODE END 3 */
}
基于EventRecoder的时间测量功能
基础部分
Chap.10 Map文件的查看(可用于分析程序大小)
名词表
Section Cross References
不同函数的调用关系
被删除的冗余函数,启用One ELF Section per Function 功能后会有
Image Symbol Table
分为两部分Local Symbols(显示 static 修饰的函数地址,变量大小),Global Symbols(显示全局变量的)
Memory Map of the image
分为加载域 (Load Region,程序存放的样子) 和运行域(Execution Region,程序运行时的样子)
Image component sizes
.text:与 RO-code 同义
.constdata: 与 RO-data 同义
.bss:与 Zl-data 同义
.data: 与 RW-data 同义
Code (inc. Data):显示代码占用了多少字节。在此映像中,有 19442 字节的代码,其中包括 1832 字节的内联数据 (inc. data),例如文字池和短字符串。
RO Data:显示只读数据占用了多少字节(比如 const char buf[] = “123456”)。这是除 Code (inc. data) 列中包括的内联数据之外的数据。
RW Data:显示读写数据占用了多少字节。
ZI Data:显示零初始化的数据占用了多少字节。
Debug:显示调试数据占用了多少字节,例如,调试输入节以及符号和字符串。
Object Totals:显示链接到一起以生成映像的对象占用了多少字节。
(incl. Generated):链接器会生成的映像内容,例如,交互操作中间代码。如果 Object Totals 行包含此类型的数据,则会显示在该行中。本例中共有 1016 字节的 RO 数据,其中 32 字节是链接器生成的 RO 数据。
(incl. Padding):链接器根据需要插入填充,以强制字节对齐。
Library Totals
下面的 Librany Totals 显示已提取并作为单个对象 添加到映像中的库成员占用了多少字节。
最后一项
Grand Totals:显示映像的真实大小。
ELF Image Totals:ELF (Executable and Linking Format) 可执行链接格式映像文件大小。
ROM Totals:显示包含映像所需的 ROM 的最小大小。这不包括 ZI 数据和存储在 ROM 中的调试信息。
htm文件的查看(用于查看工程/具体某个函数的最大栈深度)
自己看去
Chap.11 MDK移植SEGGER的硬件异常分析(jlink用的)
下载链接
认识HAL 库框架
stm32h7xx_hal_conf.h(如果不用 cubemx 的话就在这里改)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 设计的板子的外部晶振要与这个一致(25MHz)
#define HSE_VALUE (25000000UL)
// 注意滴答定时器的优先级,如果你需要在中断函数中使用HAL_Delay()时,因为delay需要基于滴答计时器进行计时,如果优先级较低会卡死。
#define TICK_INT_PRIORITY (15UL) /*!< tick interrupt priority */
// STM32H7 的 SDIO 外接支持 UHS-I 模式 (SDR12, SDR25, SDR50, SDR104 和 DDR50)
的 SD 卡, 需要 1.8 的电平转换器, 此选项就是来使能此功能用的
#define USE_SD_TRANSCEIVER 0U /*!< use uSD Transceiver */
// 使能断言函数(需要自己重写void assert_failed(uint8_t *file, uint32_t line))默认关闭
#define USE_FULL_ASSERT 1U
#ifdef USE_FULL_ASSERT
/**
* @brief The assert_param macro is used for function's parameters check.
* @param expr: If expr is false, it calls assert_failed function
* which reports the name of the source file and the source
* line number of the call that failed.
* If expr is true, it returns no value.
* @retval None
*/
#define assert_param(expr) ((expr) ? (void)0U : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
void assert_failed ( uint8_t * file , uint32_t line );
#else
#define assert_param(expr) ((void)0U)
#endif /* USE_FULL_ASSERT */
启动流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/*
******************************************************************************************************
* 函 数 名: bsp_Init
* 功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。
* 只需要调用一次
* 形 参:无
* 返 回 值: 无
******************************************************************************************************
*/
void bsp_Init ( void )
{
/* 配置MPU(内存保护) */
MPU_Config ();
/* 使能L1 Cache */
CPU_CACHE_Enable ();
/*
STM32H7xx HAL 库初始化,此时系统用的还是H7自带的64MHz,HSI时钟:
- 调用函数HAL_InitTick,初始化滴答时钟中断1ms。
- 设置NVIV优先级分组为4。
*/
HAL_Init ();
/*
配置系统时钟到400MHz
- 切换使用HSE。
- 此函数会更新全局变量SystemCoreClock,并重新配置HAL_InitTick。
- 它会开启滴答定时器中断,如果用户也要使用滴答定时器中断,此问
题就要引起注意。(见下面代码)
*/
SystemClock_Config ();
bsp_InitKey (); /* 按键初始化,要放在滴答定时器之前,因为按钮检测是通过滴答定时器扫描 */
bsp_InitTimer (); /* 初始化滴答定时器 */
bsp_InitUart (); /* 初始化串口 */
bsp_InitExtIO (); /* 初始化FMC总线74HC574扩展IO. 必须在 bsp_InitLed()前执行 */
bsp_InitLed (); /* 初始化LED */
bsp_InitLPTIMOutPWM (); /* 低功耗定时器PWM输出 */
}
/*
*********************************************************************************************************
* 函 数 名: SysTick_Handler
* 功能说明: 系统嘀嗒定时器中断服务程序。启动文件中引用了该函数。
* 形 参: 无
* 返 回 值: 无
*********************************************************************************************************
*/
void SysTick_Handler ( void )
{
HAL_IncTick (); /* ST HAL库的滴答定时中断服务程序 */
if ( g_ucEnableSystickISR == 0 ) /* bsp配置的bsp层SysTickHandle使能标志,调用了函数bsp_InitTimer才置位此变量 */
{
return ;
}
SysTick_ISR (); /* 安富莱bsp库的滴答定时中断服务程序 */
}
// 上面的函数注释
__weak void HAL_IncTick ( void )
{
uwTick += ( uint32_t ) uwTickFreq ;
}
void SysTick_ISR ( void )
{
static uint8_t s_count = 0 ;
uint8_t i ;
/* 每隔1ms进来1次 (仅用于 bsp_DelayMS) */
if ( s_uiDelayCount > 0 )
{
if ( -- s_uiDelayCount == 0 )
{
s_ucTimeOutFlag = 1 ;
}
}
/* 每隔1ms,对软件定时器的计数器进行减一操作 */
for ( i = 0 ; i < TMR_COUNT ; i ++ )
{
bsp_SoftTimerDec ( & s_tTmr [ i ]);
}
/* 全局运行时间每1ms增1 */
g_iRunTime ++ ;
if ( g_iRunTime == 0x7FFFFFFF ) /* 这个变量是 int32_t 类型,最大数为 0x7FFFFFFF */
{
g_iRunTime = 0 ;
}
bsp_RunPer1ms (); /* 每隔1ms调用一次此函数,此函数在 bsp.c */
if ( ++ s_count >= 10 )
{
s_count = 0 ;
bsp_RunPer10ms (); /* 每隔10ms调用一次此函数,此函数在 bsp.c */
}
}
HAL 库初始化外设(e.g. UART)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
HAL_UART_Init () -> __weak HAL_UART_MspInit (); // 在此进行初始化
void HAL_UART_MspInit ( UART_HandleTypeDef * huart )
{
GPIO_InitTypeDef GPIO_InitStruct = { 0 };
if ( huart -> Instance == USART1 )
{
__HAL_RCC_USART1_CLK_ENABLE ();
__HAL_RCC_GPIOB_CLK_ENABLE ();
GPIO_InitStruct . Pin = GPIO_PIN_7 ;
GPIO_InitStruct . Mode = GPIO_MODE_AF_PP ;
GPIO_InitStruct . Pull = GPIO_NOPULL ;
GPIO_InitStruct . Speed = GPIO_SPEED_FREQ_LOW ;
GPIO_InitStruct . Alternate = GPIO_AF7_USART1 ;
HAL_GPIO_Init ( GPIOB , & GPIO_InitStruct );
GPIO_InitStruct . Pin = GPIO_PIN_14 ;
GPIO_InitStruct . Mode = GPIO_MODE_AF_PP ;
GPIO_InitStruct . Pull = GPIO_NOPULL ;
GPIO_InitStruct . Speed = GPIO_SPEED_FREQ_LOW ;
GPIO_InitStruct . Alternate = GPIO_AF4_USART1 ;
HAL_GPIO_Init ( GPIOB , & GPIO_InitStruct );
}
else if ( huart -> Instance == USART2 )
{
__HAL_RCC_USART2_CLK_ENABLE ();
__HAL_RCC_GPIOD_CLK_ENABLE ();
__HAL_RCC_GPIOA_CLK_ENABLE ();
GPIO_InitStruct . Pin = GPIO_PIN_6 ;
GPIO_InitStruct . Mode = GPIO_MODE_AF_PP ;
GPIO_InitStruct . Pull = GPIO_NOPULL ;
GPIO_InitStruct . Speed = GPIO_SPEED_FREQ_LOW ;
GPIO_InitStruct . Alternate = GPIO_AF7_USART2 ;
HAL_GPIO_Init ( GPIOD , & GPIO_InitStruct );
GPIO_InitStruct . Pin = GPIO_PIN_2 ;
GPIO_InitStruct . Mode = GPIO_MODE_AF_PP ;
GPIO_InitStruct . Pull = GPIO_NOPULL ;
GPIO_InitStruct . Speed = GPIO_SPEED_FREQ_LOW ;
GPIO_InitStruct . Alternate = GPIO_AF7_USART2 ;
HAL_GPIO_Init ( GPIOA , & GPIO_InitStruct );
}
}
HAL 中断函数示例(e.g. HAL_UART_IRQHandler)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
HAL_UART_TxHalfCpltCallback () //DMA 发送模式下,发送缓冲区前半部分传输完成
HAL_UART_TxCpltCallback () //中断或 DMA 模式下,整个发送完成
HAL_UART_RxHalfCpltCallback () //DMA 接收模式下,接收缓冲区前半部分接收完成
//中断模式:单次接收完成;
//DMA 模式:整个缓冲区接收完成
HAL_UART_RxCpltCallback ()
HAL_UART_ErrorCallback ()
//调用 HAL_UART_Abort() 后,发送和接收都中止完成时调用
HAL_UART_AbortCpltCallback ( UART_HandleTypeDef * huart )
// 调用 HAL_UART_AbortTransmit() 后,发送中止完成
HAL_UART_AbortTransmitCpltCallback ( UART_HandleTypeDef * huart )
//调用 HAL_UART_AbortReceive() 后,接收中止完成
HAL_UART_AbortReceiveCpltCallback ( UART_HandleTypeDef * huart )
HAL 库的 DMA 处理思路
1
2
3
4
5
6
7
8
9
10
11
12
13
HAL_UART_Transmit_DMA ()
HAL_UART_Receive_DMA ()
HAL_UART_DMAPause ()
HAL_UART_DMAResume ()
HAL_UART_DMAStop ()
// 针对外设的 DMA 函数基本都有开启中断, 如果用户使能此外设的 NVIC, 使用
中务必别忘了写 DMA 的中断服务程序
void DMA1_Stream1_IRQHandler ( void )
{
/* 参数是 DMA 句柄 */
HAL_DMA_IRQHandler ( & hdma_usart1_tx );
}
Chap.13 STM32H7启动过程详解
启动文件分析
1
2
3
4
5
6
7
硬件复位后:
设置堆栈指针 SP = __initial_sp 。
- 设置 PC 指针 = Reset_Handler 。
- 设置中断向量表。
- 配置系统时钟。
- 配置外部 SRAM / SDRAM 用于程序变量等数据存储(这是可选的) 。
- 跳转到 C 库中的 __main ,最终会调用用户程序的 main () 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
; Amount of memory ( in bytes ) allocated for Stack
; Tailor this value to your application needs
; < h > Stack Configuration
; < o > Stack Size ( in Bytes ) < 0x0 - 0xFFFFFFFF : 8 >
; </ h >
Stack_Size EQU 0x00001000
AREA STACK , NOINIT , READWRITE , ALIGN = 3
Stack_Mem SPACE Stack_Size
__initial_sp
0x00001000 表示栈大小,注意这里是以字节为单位
ARER 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。
STACK :表示这个段的名字,可以任意命名。
NOINIT :表示此数据段不需要填入初始数据。
READWRITE :表示此段可读可写。
ALIGN = 3 :表示首地址按照 2 的 3 次方对齐,也就是按照 8 字节对齐 ( 地址对 8 求余数等于 0 )
SPACE 这行指令告诉汇编器给 STACK 段分配 0x00000400 字节的连续内存空间。
initial_sp 紧接着 SPACE 语句放置,表示了栈顶地址。 __initial_sp只是一个标号 ,标号主
要用于表示一片内存空间的某个位置
; < h > Heap Configuration
; < o > Heap Size ( in Bytes ) < 0x0 - 0xFFFFFFFF : 8 >
; </ h >
Heap_Size EQU 0x0000800
AREA HEAP , NOINIT , READWRITE , ALIGN = 3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
__heap_base 表示堆的开始地址。武汉安富莱电子有限公司
__heap_limit 表示堆的结束地址
PRESERVE8
THUMB
; Vector Table Mapped to Address 0 at Reset
AREA RESET , DATA , READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
PRESERVE8 指定当前文件保持堆栈八字节对齐。
THUMB 表示后面的指令是 THUMB 指令集 , CM7 采用的是 THUMB - 2 指令集。
AREA 定义一块数据段,只读,段名字是 RESET 。 READONLY 表示只读,缺省就表示代码段了。
3 行 EXPORT 语句将 3 个标号申明为可被外部引用, 主要提供给链接器用于连接库文件或
其他文件。
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
; External Interrupts
DCD WWDG_IRQHandler ; Window WatchDog interrupt ( wwdg1_it )
DCD PVD_AVD_IRQHandler ; PVD / AVD through EXTI Line detection
DCD TAMP_STAMP_IRQHandler ; Tamper and TimeStamps through the EXTI line
DCD RTC_WKUP_IRQHandler ; RTC Wakeup through the Reserved
DCD WAKEUP_PIN_IRQHandler ; Interrupt for all 6 wake - up pins
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
建立中断向量表,中断向量表定位在代码段的最前面。具体的物理地址由链接器的
配置参数( IROM1 的地址)决定。如果程序在 Flash 运行,则中断向量表的起始地址是 0x08000000 。
AREA | . text | , CODE , READONLY
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [ WEAK ]
IMPORT SystemInit
IMPORT __main
LDR R0 , = SystemInit
BLX R0
LDR R0 , = __main
BX R0
ENDP
第 1 行: AREA 定义一块代码段,只读,段名字是 . text 。 READONLY 表示只读。
第 4 行: 利用 PROC 、 ENDP这一对伪指令把程序段分为若干个过程 ,使程序的结构加清晰。
第 5 行: WEAK 声明其他的同名标号优先于该标号被引用 , 就是说如果外面声明了的话会调用外面的。 这个声明很重要,它让我们可以在 C文件中任意地方放置中断服务程序 ,只要保证 C 函数的名字和向量表中的名字一致即可。
第 6 行: IMPORT :伪指令用于通知编译器要使用的标号在其他的源文件中定义。但要在当前源文件中引用,而且无论当前源文件是否引用该标号,该标号均会被加入到当前源文件的符号表中。
第 9 行: SystemInit 函数在文件 system_stm32h7xx . c 里面,主要实现 RCC相关寄存器复位和中断向量表位置设置 。
第 11 行: __main 标号表示 C / C ++ 标准实时库函数里的一个初始化子程序 __main的入口地址 。该程序的一个主要作用是初始化堆栈 ( 跳转 __user_initial_stackheap标号进行初始化堆栈的 ,下面会讲到这个标号 ) ,并初始化映像文件,最后跳转到 C 程序中的 main 函数。这就解释了为何所有的 C 程序必须有一个 main 函数作为程序的起点。因为这是由 C / C ++ 标准实时库所规,并且不能更改
; Dummy Exception Handlers ( infinite loops which can be modified )
NMI_Handler PROC
EXPORT NMI_Handler [ WEAK ]
B .
ENDP
HardFault_Handler \
PROC
EXPORT HardFault_Handler [ WEAK ]
B .
ENDP
MemManage_Handler \
PROC
EXPORT MemManage_Handler [ WEAK ]
B .
ENDP
EXPORT WWDG_IRQHandler [ WEAK ]
EXPORT PVD_AVD_IRQHandler [ WEAK ]
EXPORT TAMP_STAMP_IRQHandler [ WEAK ]
EXPORT RTC_WKUP_IRQHandler [ WEAK ]
EXPORT FLASH_IRQHandler [ WEAK ]
WWDG_IRQHandler
PVD_AVD_IRQHandler
TAMP_STAMP_IRQHandler
RTC_WKUP_IRQHandler
FLASH_IRQHandler
RCC_IRQHandler
EXTI0_IRQHandler
EXTI1_IRQHandler
EXTI2_IRQHandler
EXTI3_IRQHandler
EXTI4_IRQHandler
DMA1_Stream0_IRQHandler
SAI4_IRQHandler
WAKEUP_PIN_IRQHandler
B .
ENDP
ALIGN
第 5 行: 死循环,用户可以在此实现自己的中断服务程序。 不过很少在这里实现中断服务程序,一般多是在其它的 C 文件里面重新写一个同样名字的中断服务程序, 因为这里是 WEEK 弱定义的。 如果没有在其它文件中写中断服务器程序,且使能了此中断,进入到这里后,会让程序卡在这个地方。
第 14 行: 缺省中断服务程序(开始)
第 23 行: 死循环, 如果用户使能中断服务程序,而没有在 C 文件里面写中断服务程序的话,都会进入到
这里。 比如在程序里面使能了串口 1 中断,而没有写中断服务程序 USART1_IRQHandle , 那么串口中断来了,会进入到这个死循环。
第 25 行: 缺省中断服务程序(结束) 。
; *******************************************************************************
; User Stack and Heap initialization
; *******************************************************************************
IF : DEF : __MICROLIB
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
ELSE
IMPORT __use_two_region_memory
EXPORT __user_initial_stackheap
__user_initial_stackheap
LDR R0 , = Heap_Mem
LDR R1 , = ( Stack_Mem + Stack_Size )
LDR R2 , = ( Heap_Mem + Heap_Size )
LDR R3 , = Stack_Mem
BX LR
ALIGN
ENDIF
END
第 4 行: 简单的汇编语言实现 IF …… . ELSE …………语句。如果定义了 MICROLIB ,那么程序是不会执行 ELSE分支的代码 。 __MICROLIB 可能大家并不陌生,就在 MDK 的 Target Option 里面设置。武汉安富莱电子有限公司
第 5 行: __user_initial_stackheap 将由 __main 函数进行调用。
BOOT启动模式(H7)
boot引脚:
0->启动地址由 BOOT_ADD0 定义, 默认值是 0x0800,对应Flash 首地址 0x0800 0000
1->启动地址由 BOOT_ADD1 定义, 默认值是 0x1FF0,对应系统 bootloader 的首地址0x1FF0 0000
BOOT_ADD0 和 BOOT_ADD1 对应 32 位地址到高 16 位, 这点要特别注意。 通过这两个选项字节,所有 0x0000 0000 到 0x3FFF 0000 的存储器地址都可以设置, 包括:
所有 Flash 地址空间。
所有 RAM 地址空间, ITCM, DTCM 和 SRAM。
设置了选项字节后, 掉电不会丢失, 下次上电或者复位后, 会根据 BOOT 引脚状态从 BOOT_ADD0,或 BOOT_ADD1 所设置的地址进行启动。
使用 BOOT 功能,注意以下几个问题:
如果用户不慎,设置的地址范围不在有效的存储器地址,那么 BOOT = 0 时, 会从 Flash 首地址 0x08000000 启动, BOOT = 1 时, 会从 ITCM 首地址 0x0000 0000 启动。
如果用户使能了 Flash Level 2 保护,那么只能从 Flash 地址空间进行启动。
GPIO
IO 补偿单元, 用于高速
1
2
3
4
#define GPIO_SPEED_FREQ_LOW ((uint32_t)0x00000000U) /*!< Low speed */
#define GPIO_SPEED_FREQ_MEDIUM ((uint32_t)0x00000001U) /*!< Medium speed */
#define GPIO_SPEED_FREQ_HIGH ((uint32_t)0x00000002U) /*!< Fast speed */
#define GPIO_SPEED_FREQ_VERY_HIGH ((uint32_t)0x00000003U) /*!< High speed */
不使用的引脚推荐设置为模拟模式
主要从功耗和防干扰考虑:
所有用作带上拉电阻输入的 I/O 都会在引脚外部保持为低时产生电流消耗。此电流消耗的值可通过使
用的静态特性中给出的上拉 / 下拉电阻值简单算出。
对于输出引脚,还必须考虑任何外部下拉电阻或外部负载以估计电流消耗。
若外部施加了中间电平,则额外的 I/O 电流消耗是因为配置为输入的 I/O。此电流消耗是由用于区
分输入值的输入施密特触发器电路导致。除非应用需要此特定配置,否则可通过将这些 I/O 配置为模
拟模式以避免此供电电流消耗。 ADC 输入引脚应配置为模拟输入就是这种情况。
任何浮空的输入引脚都可能由于外部电磁噪声,成为中间电平或意外切换。为防止浮空引脚相关的电
流消耗,它们必须配置为模拟模式,或内部强制为确定的数字值。这可通过使用上拉 / 下拉电阻或
将引脚配置为输出模式做到。
综上考虑,不使用的引脚设置为模拟模式, 悬空即可。
stm32h7xx_hal.c
Systick 的相关函数
1
2
3
4
5
6
7
8
__weak void HAL_IncTick ( void )
__weak uint32_t HAL_GetTick ( void )
uint32_t HAL_GetTickPrio ( void )
HAL_StatusTypeDef HAL_SetTickFreq ( HAL_TickFreqTypeDef Freq )
HAL_TickFreqTypeDef HAL_GetTickFreq ( void )
__weak void HAL_Delay ( uint32_t Delay )
__weak void HAL_SuspendTick ( void )
__weak void HAL_ResumeTick ( void )
函数 HAL_IncTick 在滴答定时器中断里面被调用,实现一个简单的计数功能,因为一般滴答定时器中
断都是配置的 1ms,所以计数全局变量 uwTick 每毫秒加 1。
函数 HAL_GetTick 用于获取全局变量 uwTick 当前的计数。
函数 HAL_GetTickPrio 用于获取滴答时钟优先级。
函数 HAL_SetTickFreq 和 HAL_GetTickFreq 是一对, 前者用于设置滴答中断频率, 后再用于获取滴答中断频率。
函数 HAL_Delay 用于阻塞式延迟,默认单位是 ms。
函数 HAL_SuspendTick 和 HAL_ResumeTick 是一对, 前者用于挂起滴答定时器, 后者用于恢复
HAL_SYSCFG_VREFBUF_VoltageScalingConfig
配置 STM32H7 内部电压基准大小
当形参 VoltageScaling = SYSCFG_VREFBUF_VOLTAGE_SCALE0 时输出基准是 2.048 V,条件是 VDDA >= 2.4V。
当形参 VoltageScaling = SYSCFG_VREFBUF_VOLTAGE_SCALE1 时输出基准是 2.5 V,条件是 VDDA >= 2.8V。
当形参 VoltageScaling = SYSCFG_VREFBUF_VOLTAGE_SCALE2 时输出基准是 1.5 V,条件是 VDDA >= 1.8V。
当形参 VoltageScaling = SYSCFG_VREFBUF_VOLTAGE_SCALE3 时输出基准是 1.8 V,条件是 VDDA >= 2.1V
HAL_SYSCFG_AnalogSwitchConfig
1
void HAL_SYSCFG_AnalogSwitchConfig ( uint32_t SYSCFG_AnalogSwitch , uint32_t SYSCFG_SwitchState )
引脚 PA0, PA1, PC2, PC3 用于 ADC 时,还有一组对应的可选引脚 PA0_C, PA1_C, PC2_C 和PC3_C。此函数的作用就是切换可选引脚。关于这个问题的详情可看此贴:
HAL_SYSCFG_CM7BootAddConfig
用于配置 BOOT = 0 或者 BOOT = 1 时的启动地址
IO 补偿相关
1
2
3
4
5
6
void HAL_EnableCompensationCell ( void )
void HAL_DisableCompensationCell ( void ) // 分别用于使能或者禁止 IO 补偿,只有在供电电压范围是 2.4V 到 3.6V 之间时,使用此功能才有意义
void HAL_SYSCFG_EnableIOSpeedOptimize ( void )
void HAL_SYSCFG_DisableIOSpeedOptimize ( void ) // 分别用于优化 IO 速度或者禁止优化,不过仅在供电电压低于 2.5V 时可用,高于 2.5V 是不可以使用的,另外使用这个功能的前提是用户使能了 PRODUCT_BELOW_25V(是可选字节配置选项里面的一个 bit) 。
void HAL_SYSCFG_CompensationCodeSelect ( uint32_t SYSCFG_CompCode ) // IO 补偿单元的选择, 函数形参可以是 SYSCFG_CELL_CODE,即寄存器 SYSCFG_CCVR,也可以是SYSCFG_REGISTER_CODE,即寄存器 SYSCFG_CCCR。
void HAL_SYSCFG_CompensationCodeConfig ( uint32_t SYSCFG_PMOSCode , uint32_t SYSCFG_NMOSCode ) // 用于设置 GPIO 内部构造中 NMOS 和 PMOS 的补偿值,两个形参的范围都是 0-15。根据用户调用函数 HAL_SYSCFG_CompensationCodeSelect 选择的寄存器,这里仅有一个形参的设置是有效的。
stm32h7xx_hal_rcc.c
BASE
HSI (high-speed internal):高速内部 RC 振荡器,可以直接或者通过 PLL 倍频后做系统时钟源。缺点是精度差些,即使经过校准。
CSI (Low-power internal oscillator):低功耗内部 RC 振荡器,作用同 HSI,不过主要用于低功耗。
LSI (low-speed internal):低速内部时钟,主要用于独立看门狗和 RTC 的时钟源。
HSE (high-speed external):高速外部晶振,可接 4 - 48MHz 的晶振,可以直接或者通过 PLL 倍频后做系统时钟源,也可以做 RTC 的是时钟源。
LSE (low-speed external):低速外部晶振, 主要用于 RTC。
CSS (Clock security system):时钟安全系统,一旦使能后,如果 HSE 启动失败(不管是直接作为系统时钟源还是通过 PLL 输出后做系统时钟源),系统时钟将切换到 HSI。如果使能了中断的话,将进入不可屏蔽中断 NMI。
MCO1 (micro controller clock output):可以在 PA8 引脚输出 HSI, LSE, HSE, PLL1(PLL1_Q)或 HSI48 时钟。
MCO2 (micro controller clock output):可以在 PC9 引脚输出 HSE, PLL2(PLL2_P), SYSCLK, LSI, CSI 或 PLL1(PLL1_P) 时钟。
PLL 锁相环,时钟输入来自 HSI , HSE 或者 CSI: 主锁相环 PLL1,用于给 CPU 和部分外设提供时钟。专用锁相环 PLL2 和 PLL3,用于给部分外设提供时钟
HAL_RCC_ClockConfig
配置了 CPU、 AHB 和 APB 的所有总线时钟,即 HCLK, SYSTICK, D1PCLK1, PCLK1, PCLK2 和 D3PCLK1 时钟
注意事项 :
此函数会更新全局变量 SystemCoreClock 的主频值,并且会再次调用函数 HAL_InitTick 更新系统滴答时钟,这点要特别注意。
系统上电复位或者从停机、待机模式唤醒后,使用的是 HSI 作为系统时钟。以防使用 HSE 直接或者通过 PLL 输出后做系统时钟时失败(如果使能了 CSS) 。
目标时钟就绪后才可以从当前的时钟源往这个目标时钟源切换,如果目标时钟源没有就绪,就会等待直到时钟源就绪才可以切换。
根据设备的供电范围,必须正确设置 D1CPRE[3:0]位的范围,防止超过允许的最大频率。
HAL_RCC_MCOConfig
配置 MCO1(PA8 引脚) 和 MCO2(PC9 引脚)的时钟输出以及选择的时钟源
stm32h7xx_hal_cortex.c
HAL 库的 GPIO 驱动
第 1 步: 使能 GPIO 所在总线的 AHB 时钟, __HAL_RCC_GPIOx_CLK_ENABLE()。
第 2 步: 通过函数 HAL_GPIO_Init()配置 GPIO。
通过结构体 GPIO_InitTypeDef 的成员 Mode 配置输入、 输出、 模拟等模式。
通过结构体 GPIO_InitTypeDef 的成员 Pull 配置上拉、下拉电阻。
通过结构体 GPIO_InitTypeDef 的成员 Speed 配置 GPIO 速度等级。
如果选择了复用模式,那么就需要配置结构体 GPIO_InitTypeDef 的成员 Alternate。
如果引脚功能用于 ADC、 DAC 的话,需要配置引脚为模拟模式。
如果是用于外部中断/事件,结构体 GPIO_InitTypeDef 的成员 Mode 可以配置相应模式,相应的上升沿、 下降沿或者双沿触发也可以选择。
第 3 步: 如果配置了外部中断/事件,可以通过函数 HAL_NVIC_SetPriority 设置优先级,然后调用函数 HAL_NVIC_EnableIRQ 使能此中断。
第 4 步: 输入模式读取引脚状态可以使用函数 HAL_GPIO_ReadPin。
第 5 步: 输出模式设置引脚状态可以调用函数 HAL_GPIO_WritePin()和 HAL_GPIO_TogglePin。
另外注意下面三个问题:
系统上电复位后, GPIO 默认是模拟模式,除了 JTAG 相关引脚。
关闭 LSE 的话,用到的两个引脚 OSC32_IN 和 OSC32_OUT(分别是 PC14, PC15)可以用在通用IO,如果开启了,就不能再做 GPIO。
关闭 HSE 的话,用到的两个引脚 OSC_IN 和 OSC_OUT(分别是 PH0, PH1)可以用在通用 IO,如果开启了,就不能再做 GPIO。
按键 FIFO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
#define KEY_FILTER_TIME 5
#define KEY_LONG_TIME 100 /* 单位10ms, 持续1秒,认为长按事件 */
#define ALL_KEY_GPIO_CLK_ENABLE() { \
__HAL_RCC_GPIOB_CLK_ENABLE(); \
__HAL_RCC_GPIOC_CLK_ENABLE(); \
__HAL_RCC_GPIOG_CLK_ENABLE(); \
__HAL_RCC_GPIOH_CLK_ENABLE(); \
__HAL_RCC_GPIOI_CLK_ENABLE(); \
};
// 初始化
typedef struct
{
/* 下面是一个函数指针,指向判断按键手否按下的函数 */
uint8_t ( * IsKeyDownFunc )( void ); /* 按键按下的判断函数,1表示按下 */
uint8_t Count ; /* 滤波器计数器 */
uint16_t LongCount ; /* 长按计数器 */
uint16_t LongTime ; /* 按键按下持续时间, 0表示不检测长按 */
uint8_t State ; /* 按键当前状态(按下还是弹起) */
uint8_t RepeatSpeed ; /* 连续按键周期 */
uint8_t RepeatCount ; /* 连续按键计数器 */
} KEY_T ;
#define KEY_FIFO_SIZE 10
typedef struct
{
uint8_t Buf [ KEY_FIFO_SIZE ]; /* 键值缓冲区 */
uint8_t Read ; /* 缓冲区读指针1 */
uint8_t Write ; /* 缓冲区写指针 */
uint8_t Read2 ; /* 缓冲区读指针2 */
} KEY_FIFO_T ;
typedef struct
{
GPIO_TypeDef * gpio ;
uint16_t pin ;
uint8_t ActiveLevel ; /* 激活电平 */
} X_GPIO_T ;
// 注册
static const X_GPIO_T s_gpio_list [ HARD_KEY_NUM ] = {
{ GPIOI , GPIO_PIN_8 , 0 }, /* K1 */
{ GPIOC , GPIO_PIN_13 , 0 }, /* K2 */
{ GPIOH , GPIO_PIN_4 , 0 }, /* K3 */
{ GPIOG , GPIO_PIN_2 , 0 }, /* JOY_U */
{ GPIOB , GPIO_PIN_0 , 0 }, /* JOY_D */
{ GPIOG , GPIO_PIN_3 , 0 }, /* JOY_L */
{ GPIOG , GPIO_PIN_7 , 0 }, /* JOY_R */
{ GPIOI , GPIO_PIN_11 , 0 }, /* JOY_OK */
};
void bsp_InitKey ( void );
void bsp_KeyScan10ms ( void );
void bsp_PutKey ( uint8_t _KeyCode );
uint8_t bsp_GetKey ( void );
uint8_t bsp_GetKey2 ( void );
uint8_t bsp_GetKeyState ( KEY_ID_E _ucKeyID );
void bsp_SetKeyParam ( uint8_t _ucKeyID , uint16_t _LongTime , uint8_t _RepeatSpeed );
void bsp_ClearKey ( void );
void bsp_InitKey ( void )
{
bsp_InitKeyVar (); /* 初始化按键变量 */
bsp_InitKeyHard (); /* 初始化按键硬件 */
}
static void bsp_InitKeyHard ( void )
{
GPIO_InitTypeDef gpio_init ;
uint8_t i ;
/* 第1步:打开GPIO时钟 */
ALL_KEY_GPIO_CLK_ENABLE ();
/* 第2步:配置所有的按键GPIO为浮动输入模式(实际上CPU复位后就是输入状态) */
gpio_init . Mode = GPIO_MODE_INPUT ; /* 设置输入 */
gpio_init . Pull = GPIO_NOPULL ; /* 上下拉电阻不使能 */
gpio_init . Speed = GPIO_SPEED_FREQ_VERY_HIGH ; /* GPIO速度等级 */
for ( i = 0 ; i < HARD_KEY_NUM ; i ++ )
{
gpio_init . Pin = s_gpio_list [ i ]. pin ;
HAL_GPIO_Init ( s_gpio_list [ i ]. gpio , & gpio_init );
}
}
static void bsp_InitKeyVar ( void )
{
uint8_t i ;
/* 对按键FIFO读写指针清零 */
s_tKey . Read = 0 ;
s_tKey . Write = 0 ;
s_tKey . Read2 = 0 ;
/* 给每个按键结构体成员变量赋一组缺省值 */
for ( i = 0 ; i < KEY_COUNT ; i ++ )
{
s_tBtn [ i ]. LongTime = KEY_LONG_TIME ; /* 长按时间 0 表示不检测长按键事件 */
s_tBtn [ i ]. Count = KEY_FILTER_TIME / 2 ; /* 计数器设置为滤波时间的一半 */
s_tBtn [ i ]. State = 0 ; /* 按键缺省状态,0为未按下 */
s_tBtn [ i ]. RepeatSpeed = 0 ; /* 按键连发的速度,0表示不支持连发 */
s_tBtn [ i ]. RepeatCount = 0 ; /* 连发计数器 */
}
/* 如果需要单独更改某个按键的参数,可以在此单独重新赋值 */
/* 摇杆上下左右,支持长按1秒后,自动连发 */
bsp_SetKeyParam ( KID_JOY_U , 100 , 6 );
bsp_SetKeyParam ( KID_JOY_D , 100 , 6 );
bsp_SetKeyParam ( KID_JOY_L , 100 , 6 );
bsp_SetKeyParam ( KID_JOY_R , 100 , 6 );
}
// 下面只pin一些重要的
//判断按键是否按下
static uint8_t KeyPinActive ( uint8_t _id )
{
uint8_t level ;
if (( s_gpio_list [ _id ]. gpio -> IDR & s_gpio_list [ _id ]. pin ) == 0 ){
level = 0 ;
} else {
level = 1 ;
}
if ( level == s_gpio_list [ _id ]. ActiveLevel ){
return 1 ;
} else {
return 0 ;
}
}
// 封装KeyPinActive,支持判断多个按键同时按下的情况
static uint8_t IsKeyDownFunc ( uint8_t _id )
{
/* 实体单键 */
if ( _id < HARD_KEY_NUM )
{
uint8_t i ;
uint8_t count = 0 ;
uint8_t save = 255 ;
/* 判断有几个键按下 */
for ( i = 0 ; i < HARD_KEY_NUM ; i ++ )
{
if ( KeyPinActive ( i ))
{
count ++ ;
save = i ;
}
}
if ( count == 1 && save == _id )
{
return 1 ; /* 只有1个键按下时才有效 */
}
return 0 ;
}
/* 组合键 K1K2 */
if ( _id == HARD_KEY_NUM + 0 )
{
if ( KeyPinActive ( KID_K1 ) && KeyPinActive ( KID_K2 ))
{
return 1 ;
}
else
{
return 0 ;
}
}
return 0 ;
}
// 数据入队
void bsp_PutKey ( uint8_t _KeyCode )
{
s_tKey . Buf [ s_tKey . Write ] = _KeyCode ;
if ( ++ s_tKey . Write >= KEY_FIFO_SIZE )
{
s_tKey . Write = 0 ;
}
}
// 数据出队(消费)
uint8_t bsp_GetKey ( void )
{
uint8_t ret ;
if ( s_tKey . Read == s_tKey . Write )
{
return KEY_NONE ;
}
else
{
ret = s_tKey . Buf [ s_tKey . Read ];
if ( ++ s_tKey . Read >= KEY_FIFO_SIZE )
{
s_tKey . Read = 0 ;
}
return ret ;
}
}
// 直接读取状态
uint8_t bsp_GetKeyState ( KEY_ID_E _ucKeyID )
{
return s_tBtn [ _ucKeyID ]. State ;
}
// 带消抖的单个检测
static void bsp_DetectKey ( uint8_t i )
{
KEY_T * pBtn ;
pBtn = & s_tBtn [ i ];
if ( IsKeyDownFunc ( i ))
{
if ( pBtn -> Count < KEY_FILTER_TIME )
{
pBtn -> Count = KEY_FILTER_TIME ; // 让它在T-2T之间抖动
}
else if ( pBtn -> Count < 2 * KEY_FILTER_TIME )
{
pBtn -> Count ++ ; // 当按下后开始计数,如果大于它说明按下
}
else
{
if ( pBtn -> State == 0 )
{
pBtn -> State = 1 ;
/* 发送按钮按下的消息 */
bsp_PutKey (( uint8_t )( 3 * i + 1 ));
}
if ( pBtn -> LongTime > 0 )
{
if ( pBtn -> LongCount < pBtn -> LongTime )
{
/* 发送按钮持续按下的消息 */
if ( ++ pBtn -> LongCount == pBtn -> LongTime )
{
/* 键值放入按键FIFO */
bsp_PutKey (( uint8_t )( 3 * i + 3 ));
}
}
else
{
if ( pBtn -> RepeatSpeed > 0 )
{
if ( ++ pBtn -> RepeatCount >= pBtn -> RepeatSpeed )
{
pBtn -> RepeatCount = 0 ;
/* 常按键后,每隔10ms发送1个按键 */
bsp_PutKey (( uint8_t )( 3 * i + 1 ));
}
}
}
}
}
}
else // 如果停止按下
{
if ( pBtn -> Count > KEY_FILTER_TIME )
{
pBtn -> Count = KEY_FILTER_TIME ; // 消抖,防止快速回落
}
else if ( pBtn -> Count != 0 )
{
pBtn -> Count -- ; // 如果一直没有按下,则慢慢减少
}
else
{
if ( pBtn -> State == 1 )
{
pBtn -> State = 0 ;
/* 发送按钮弹起的消息 */
bsp_PutKey (( uint8_t )( 3 * i + 2 ));
}
}
pBtn -> LongCount = 0 ;
pBtn -> RepeatCount = 0 ;
}
}
// 基于SysTick中断的扫描函数
void bsp_KeyScan10ms ( void )
{
uint8_t i ;
for ( i = 0 ; i < KEY_COUNT ; i ++ )
{
bsp_DetectKey ( i );
}
}
PWM(无源蜂鸣器)
NVIC 中断分组
STM32 支持 5 种优先级分组。 系统上电复位后, 默认使用的是优先级分组 0
具有高抢占式优先级的中断可以在具有低抢占式优先级的中断服务程序执行过程中被响应,即中断嵌套,或者说高抢占式优先级的中断可以抢占低抢占式优先级的中断的执行。
在抢占式优先级相同的情况下,有几个子优先级不同的中断同时到来,那么高子优先级的中断优先被响应。
在抢占式优先级相同的情况下,如果有低子优先级中断正在执行,高子优先级的中断要等待已被响应的低子优先级中断执行结束后才能得到响应 , 即子优先级不支持中断嵌套。
Reset、 NMI、 Hard Fault 优先级为负数, 高于普通中断优先级, 且优先级不可配置。
初学者还有一个比较纠结的问题, 就是系统中断(比如: PendSV, SVC, SysTick)是不是一定比外部中断(比如 SPI,USART)要高。答案:不是的,它们是在同一个 NVIC 下面设置的。
掌握了这些基础知识基本就够用了。 另外特别注意一点,配置抢占优先级和子优先级, 它们合并成的4bit 数字的数值越小,优先级越高 ,这一点千万不要搞错了。
HAL_NVIC_SetPriorityGrouping
设置中断组
HAL_NVIC_SetPriority
设置抢占优先级以及子优先级(需要先设置优先级分组)
HAL_NVIC_EnableIRQ
使能中断
SysTick
一个 24位的递减计数器,支持中断
将重装载值写入 LOAD 寄存器(24 位)
启动后,硬件自动将 LOAD 值复制到 VAL(当前值)寄存器
VAL 每个内核时钟周期递减 1
当 VAL 减到 0:
触发 SysTick 异常(中断)
自动重新加载 LOAD 值到 VAL,开始下一轮计数
SysTick_Config(u32_t ticks)
函数的形参表示内核时钟多少个周期后触发一次 Systick 定时中断,比如形参配置为如下数值。
– SystemCoreClock / 1000 表示定时频率为 1000Hz, 也就是定时周期为 1ms。
– SystemCoreClock / 500 表示定时频率为 500Hz, 也就是定时周期为 2ms。
– SystemCoreClock / 2000 表示定时频率为 2000Hz, 也就是定时周期为 500us。
多组软定时器驱动设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#define TMR_COUNT 4 /* 软件定时器的个数 (定时器ID范围 0 - 3) */
/* 定时器结构体,成员变量必须是 volatile, 否则C编译器优化时可能有问题 */
typedef enum
{
TMR_ONCE_MODE = 0 , /* 一次工作模式 */
TMR_AUTO_MODE = 1 /* 自动定时工作模式 */
} TMR_MODE_E ;
/* 定时器结构体,成员变量必须是 volatile, 否则C编译器优化时可能有问题 */
typedef struct
{
volatile uint8_t Mode ; /* 计数器模式,1次性 */
volatile uint8_t Flag ; /* 定时到达标志 */
volatile uint32_t Count ; /* 计数器 */
volatile uint32_t PreLoad ; /* 计数器预装值 */
} SOFT_TMR ;
/* 提供给其他C文件调用的函数 */
void bsp_InitTimer ( void );
void bsp_DelayMS ( uint32_t n );
void bsp_DelayUS ( uint32_t n );
void bsp_StartTimer ( uint8_t _id , uint32_t _period );
void bsp_StartAutoTimer ( uint8_t _id , uint32_t _period );
void bsp_StopTimer ( uint8_t _id );
uint8_t bsp_CheckTimer ( uint8_t _id );
int32_t bsp_GetRunTime ( void );
int32_t bsp_CheckRunTime ( int32_t _LastTime );
void bsp_InitHardTimer ( void );
void bsp_StartHardTimer ( uint8_t _CC , uint32_t _uiTimeOut , void * _pCallBack );
void bsp_StartTimer ( uint8_t _id , uint32_t _period )
{
if ( _id >= TMR_COUNT )
{
/* 打印出错的源代码文件名、函数名称 */
BSP_Printf ( "Error: file %s, function %s() \r\n " , __FILE__ , __FUNCTION__ );
while ( 1 ); /* 参数异常,死机等待看门狗复位 */
}
DISABLE_INT (); /* 关中断 */
s_tTmr [ _id ]. Count = _period ; /* 实时计数器初值 */
s_tTmr [ _id ]. PreLoad = _period ; /* 计数器自动重装值,仅自动模式起作用 */
s_tTmr [ _id ]. Flag = 0 ; /* 定时时间到标志 */
s_tTmr [ _id ]. Mode = TMR_ONCE_MODE ; /* 1次性工作模式 */
ENABLE_INT (); /* 开中断 */
}
void bsp_StartAutoTimer ( uint8_t _id , uint32_t _period )
{
if ( _id >= TMR_COUNT )
{
/* 打印出错的源代码文件名、函数名称 */
BSP_Printf ( "Error: file %s, function %s() \r\n " , __FILE__ , __FUNCTION__ );
while ( 1 ); /* 参数异常,死机等待看门狗复位 */
}
DISABLE_INT (); /* 关中断 */
s_tTmr [ _id ]. Count = _period ; /* 实时计数器初值 */
s_tTmr [ _id ]. PreLoad = _period ; /* 计数器自动重装值,仅自动模式起作用 */
s_tTmr [ _id ]. Flag = 0 ; /* 定时时间到标志 */
s_tTmr [ _id ]. Mode = TMR_AUTO_MODE ; /* 自动工作模式 */
ENABLE_INT (); /* 开中断 */
}
void bsp_StopTimer ( uint8_t _id )
{
if ( _id >= TMR_COUNT )
{
/* 打印出错的源代码文件名、函数名称 */
BSP_Printf ( "Error: file %s, function %s() \r\n " , __FILE__ , __FUNCTION__ );
while ( 1 ); /* 参数异常,死机等待看门狗复位 */
}
DISABLE_INT (); /* 关中断 */
s_tTmr [ _id ]. Count = 0 ; /* 实时计数器初值 */
s_tTmr [ _id ]. Flag = 0 ; /* 定时时间到标志 */
s_tTmr [ _id ]. Mode = TMR_ONCE_MODE ; /* 自动工作模式 */
ENABLE_INT (); /* 开中断 */
}
uint8_t bsp_CheckTimer ( uint8_t _id )
{
if ( _id >= TMR_COUNT )
{
return 0 ;
}
if ( s_tTmr [ _id ]. Flag == 1 )
{
s_tTmr [ _id ]. Flag = 0 ;
return 1 ;
}
else
{
return 0 ;
}
}
void SysTick_ISR ( void )
{
static uint8_t s_count = 0 ;
uint8_t i ;
/* 每隔1ms进来1次 (仅用于 bsp_DelayMS) */
if ( s_uiDelayCount > 0 )
{
if ( -- s_uiDelayCount == 0 )
{
s_ucTimeOutFlag = 1 ;
}
}
/* 每隔1ms,对软件定时器的计数器进行减一操作 */
for ( i = 0 ; i < TMR_COUNT ; i ++ )
{
bsp_SoftTimerDec ( & s_tTmr [ i ]);
}
/* 全局运行时间每1ms增1 */
g_iRunTime ++ ;
if ( g_iRunTime == 0x7FFFFFFF ) /* 这个变量是 int32_t 类型,最大数为 0x7FFFFFFF */
{
g_iRunTime = 0 ;
}
bsp_RunPer1ms (); /* 每隔1ms调用一次此函数,此函数在 bsp.c */
if ( ++ s_count >= 10 )
{
s_count = 0 ;
bsp_RunPer10ms (); /* 每隔10ms调用一次此函数,此函数在 bsp.c */
}
}
MPU 内存保护单元
What
MPU 可以将 memory map 内存映射区分为多个具有一定访问规则的区域, 通过这些规则可以实现如下功能:
防止不受信任的应用程序访问受保护的内存区域。
防止用户应用程序破坏操作系统使用的数据。
通过阻止任务访问其它任务的数据区。
允许将内存区域定义为只读,以便保护重要数据。
检测意外的内存访问。
简单的说就是内存保护、 外设保护和代码访问保护。
MPU 可以配置保护 16 个内存区域(这 16 个内存域是独立配置的) ,每个区域最小要求 256 字节,每个区域还可以配置为 8 个子区域。由于子区域一般都相同大小,这样每个子区域的大小就是 32 字节,正好跟 Cache 的 Cache Line 大小一样.
MPU 可以配置的 16 个内存区的序号范围是 0 到 15,还有默认区 default region,也叫作背景区,序号-1。由于这些内存区可以嵌套和重叠,所以这些区域在嵌套或者重叠的时候有个优先级的问题。序号15 的优先级最高,以此递减,序号-1,即背景区的优先级最低 。这些优先级是固定的。
Cache 解读
注意:H7的 read allocate 是一直开启的
四种Cache模式:
缓存策略 (Cache Policy)
简明区别说明
Non-cacheable
数据不缓存,每次读写都直接访问主存。
Write back, write and read allocate
写操作先写入缓存(脏数据),读写时都分配缓存行(常见于高性能CPU)。
Write through, no write allocate
写操作同时更新缓存和主存,但写时不分配新缓存行(适合一致性要求高的场景)。
Write back, no write allocate
写操作只写入缓存(脏数据),但写时不分配新缓存行(节省空间,适合写密集型应用)。
** 注意当开启Share(多核共享)时缓存会有性能的下降,这是不必开启Cache **
面对繁冗复杂的 Cache 配置, 推荐方式和安全隐患解决办法
推荐使用 128KB 的 TCM 作为主 RAM 区 ,其它的专门用于大缓冲和 DMA 操作等。
Cache 问题主要是 CPU 和 DMA 都操作这个缓冲区时容易出现 ,使用时要注意。
Cache 配置的选择,优先考虑的是 Write Back ,然后是 Write Through 和关闭 Cache ,其中 WB 和 WT 的使用中可以配合 ARM 提供的函数解决上面说到的隐患问题 。但不是万能的,在不起作用的时候,直接暴力选择函数 SCB_CleanInvlaidateDCache 解决。关于这个问题,在分别配置以太网MAC 的描述符缓冲区,发送缓冲区和接收缓冲区时尤其突出。
Cache 相关的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
SCB_EnableICache ()
SCB_DisableICache ()
SCB_InvalidateICache ()
SCB_EnableDCache ()
SCB_DisableDCache ()
SCB_InvalidateDCache ()
SCB_CleanDCache ()
// Clean+ Invaildate两个函数一起用
SCB_CleanInvalidateDCache ()
SCB_InvalidateDCache_by_Addr ()
SCB_CleanDCache_by_Addr ()
SCB_CleanInvalidateDCache_by_Addr ()
五种内存区域讲解
TCM 区
TCM : Tightly-Coupled Memory 紧密耦合内存 。 ITCM 用于运行指令, 也就是程序代码, DTCM用于数据存取 ,特点是跟内核速度一样,而片上 RAM 的速度基本都达不到这个速度,所以有降频处理。
速度: 400MHz。
DTCM 地址: 0x2000 0000, 大小 128KB。
ITCM 地址: 0x0000 0000, 大小 64KB。
AXI SRAM 区
位于 D1 域, 数据带宽是 64bit, 挂在 AXI 总线上。 除了 D3 域中的 BDMB 主控不能访问 ,其它都可以访问此 RAM 区。
速度: 200MHz。
地址: 0x2400 0000, 大小 512KB。
用途:用途不限,可以用于用户应用数据存储或者 LCD 显存。
SRAM1, SRAM2 和 SRAM3 区
位于 D2 域, 数据带宽是 32bit, 挂在 AHB 总线上。 除了 D3 域中的 BDMB 主控不能访问这三块 SRAM ,其它都可以访问这几个 RAM 区。
速度: 200MHz。
SRAM1: 地址 0x3000 0000, 大小 128KB, 用途不限,可用于 D2 域中的 DMA 缓冲, 也可以当D1 域断电后用于运行程序代码。
SRAM2:地址 0x3002 0000, 大小 128KB, 用途不限,可用于 D2 域中的 DMA 缓冲,也可以用于用户数据存取。
SRAM3:地址 0x3004 0000, 大小 32KB, 用途不限, 主要用于以太网和 USB 的缓冲。
SRAM4 区
位于 D3 域, 数据带宽是 32bit,挂在 AHB 总线上,大部分主控都能访这块 SRAM 区。
速度: 200MHz。
地址: 0x3800 0000, 大小 64KB。
用途:用途不限,可以用于 D3 域中的 DMA 缓冲 ,也可以当 D1 和 D2 域进入 DStandby 待机方式后, 继续保存用户数据。
Backup SRAM 区
备份 RAM 区, 位于 D3 域, 数据带宽是 32bit,挂在 AHB 总线上,大部分主控都能访问这块 SRAM区。
速度: 200MHz。
地址: 0x3880 0000, 大小 4KB。
用途:用途不限, 主要用于系统进入低功耗模式后, 继续保存数据(Vbat 引脚外接电池)。
建议手动使能SRAM1-3
AXI SRAM, SRAM4, ITCM 和 DTCM 可以在上电后直接使用。 而 SRAM1, SRAM2, SRAM3是需要使能的,但是实际测试发现,不使能也可以正常使用。 不过, 建议用到时候开启下时钟, 防止意想不到的问题发生。
1
2
3
4
5
#if 0
__HAL_RCC_D2SRAM1_CLK_ENABLE();
__HAL_RCC_D2SRAM2_CLK_ENABLE();
__HAL_RCC_D2SRAM3_CLK_ENABLE();
#endif
如何使用 SRAM
自定义分散文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
LR_IROM1 0x08000000 0x00200000 { ; load region size_region
ER_IROM1 0x08000000 0x00200000 { ; load address = execution address
* o ( RESET , + First )
* ( InRoot $$ Sections )
. ANY ( + RO )
}
}
; RW data - 128 KB DTCM
RW_IRAM1 0x20000000 0x00020000 {
. ANY ( + RW + ZI )
}
; RW data - 512 KB AXI SRAM
RW_IRAM2 0x24000000 0x00080000 {
* ( RAM_D1 )
}
; RW data - 128 KB SRAM1 ( 0x30000000 ) + 128 KB SRAM2 ( 0x3002 0000 ) + 32 KB SRAM3 ( 0x30040000 )
RW_IRAM3 0x30000000 0x00048000 {
* ( RAM_D2 )
}
; RW data - 64 KB SRAM4 ( 0x38000000 )
RW_IRAM4 0x38000000 0x00010000 {
* ( RAM_D3 )
}
应用内使用
1
2
3
4
5
// 将一个大数组放入 512KB 的 AXI SRAM (RAM_D1)
__attribute__ (( section ( "RAM_D1" ))) float signal_buffer [ 16384 ];
// 将一个结构体放入 288KB 的 D2 域 SRAM (RAM_D2)
__attribute__ (( section ( "RAM_D2" ))) my_peripheral_state_t device_state ;
移植RTX5库动态分配内存
使用场景:在实际项目中有一定的实用价值,比如 MP3 编解码, JPEG 编解码, 视频播放器, 矢量字体等需要动态内存的场合。
需要使用文件:
rtx_lib.h
rtx_memory.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* DTCM, 64KB的 */
/* 用于获取当前使用的空间大小 */
mem_head_t * DTCMUsed ;
/* 定义为64位变量,首地址是8字节对齐 */
uint64_t AppMallocDTCM [ 64 * 1024 / 8 ];
/* D1域, AXI SRAM, 512KB */
/* 用于获取当前使用的空间大小 */
mem_head_t * AXISRAMUsed ;
/* 定义为64位变量,首地址是8字节对齐 */
uint64_t AppMallocAXISRAM [ 512 * 1024 / 8 ] __attribute__ (( at ( 0x24000000 )));
osRtxMemoryInit ( AppMallocDTCM , sizeof ( AppMallocDTCM ));
osRtxMemoryInit ( AppMallocAXISRAM , sizeof ( AppMallocAXISRAM ));
uint32_t * DTCM_Address0 , * AXISRAM_Address0 ;
/* 从DTCM申请280字节空间,使用指针变量DTCM_Address0操作这些空间时不要超过280字节大小 */
DTCM_Address0 = osRtxMemoryAlloc ( AppMallocDTCM , 280 , 0 );
DTCMUsed = MemHeadPtr ( AppMallocDTCM );
printf ( "DTCM总大小 = %d 字节,申请大小 = 0280字节,当前共使用大小 = %d 字节 \r\n " ,
DTCMUsed -> size , DTCMUsed -> used );
/* 从AXI SRAM申请160字节空间,使用指针变量AXISRAM_Address0操作这些空间时不要超过160字节大小 */
AXISRAM_Address0 = osRtxMemoryAlloc ( AppMallocAXISRAM , 160 , 0 );
AXISRAMUsed = MemHeadPtr ( AppMallocAXISRAM );
printf ( "AXI SRAM总大小 = %d 字节,申请大小 = 0162字节,当前共使用大小 = %d 字节 \r\n " ,
AXISRAMUsed -> size , AXISRAMUsed -> used );
/* 释放从DTCM申请的280字节空间 */
osRtxMemoryFree ( AppMallocDTCM , DTCM_Address0 );
DTCMUsed = MemHeadPtr ( AppMallocDTCM );
printf ( "释放DTCM动态内存区申请的0280字节,当前共使用大小 = %d 字节 \r\n " , DTCMUsed -> used );
/* 释放从AXI SRAM申请的160字节空间 */
osRtxMemoryFree ( AppMallocAXISRAM , AXISRAM_Address0 );
AXISRAMUsed = MemHeadPtr ( AppMallocAXISRAM );
printf ( "释放AXI SRAM动态内存区申请的0160字节,当前共使用大小 = %d 字节 \r\n " , AXISRAMUsed -> used );
关键代码在 ITCM 执行
配置链接到ITCM(会自动执行从FLASH区复制到ITCM区的操作)
复制中断向量表到 DTCM
1
2
3
4
5
6
7
8
int main (){
uint32_t * SouceAddr = ( uint32_t * ) FLASH_BANK1_BASE ;
uint32_t * DestAddr = ( uint32_t * ) D1_DTCMRAM_BASE ;
memcpy ( DestAddr , SouceAddr , 0x400 );
/* 设置中断向量表到 ITCM 里面 */
SCB -> VTOR = D1_DTCMRAM_BASE ;
MAIN_RAM ();
}
局限
ITCM用起来也不是完美的,当涉及较多的函数间调用时,如果指令地址频繁在Flash、ITCM直接来回切换,会产生许多不必要的开销,因为它们两个区域的地址跨度太大,无法直接跳转,编译器会产生一些虚拟函数来实现这个跳转的过渡,无论是Flash到ITCM,还是ITCM到Flash都会有这样的操作。
ITCM主要是用于执行一些时间敏感的函数,例如中断函数,使得中断能够更快响应。还有就是例如FFT这种纯粹的计算型函数,它不会发生较多的其它函数调用且函数执行时间较长,这也能发挥ITCM的关键优势。
USART串口
intro
STM32H7 的串口比 STM32F4 和 F1 的串口支持了更多高级特性。 比如超时接收检测、自适应波特率、 TX 和 RX 引脚互换等功能。
任意波特率。硬件采用分数波特率发生器系统,可以设置任意的波特率,最高达 4.5Mbits/s。这一点很重要。比如 ES8266 串口 WIFI 芯片,上电时有个奇怪的波特率 74880bps,当然 STM32 是可以支持的。
可编程数据字长度, 支持 7bit, 8bit 和 9bit。
可配置的停止位。支持 1 或 2 个停止位。
发送器和接收器可以单独使能。比如 GPS 应用只需要串口接收,那么发送的 GPIO 就可以节省出来用作其他功能。
检测标志和中断:
接收缓冲器满,可产生中断。串口中断服务程序据此判断是否接收到数据。
发送缓冲器空,可产生中断。串口中断服务程序据此启动发送下一个数据。
传输结束标志,可产生中断。用于 RS485 通信,等最后一个字节发送完毕后,需要控制 RS485收发器芯片切换为接收模式。其它中断不常用,包括: CTS 改变、 LIN 断开符检测、检测到总线为空闲(在 DMA 不定长接收方式会用到)、溢出错误、帧错误、噪音错误、校验错误。
H7 Features:
数据逻辑电平翻转。
RX 和 TX 引脚交换。
超时接收特性。
MSB 位先发送。
自适应波特率。
外接 485 的 PHY 芯片时, 硬件支持收发切换,无需用户手动控制 DE 引脚
重要的寄存器
标志宏
描述
置位条件
清零方式
中断使能位(在USART_CR1中)
状态含义
USART_FLAG_TXE
TXE:发送数据寄存器空 (Transmit data register empty)
当TDR寄存器中的数据被硬件转移到移位寄存器的时候,该位被硬件置位。
对USART_TDR的写操作,或将USART_RQR的TXFRQ位置1。
TXEIE
0: 数据未转移;1: 数据已转移
USART_FLAG_TC
TC: 发送完成 (Transmission complete)
当包含数据的一帧发送完成后,并且TXE=1时,由硬件将该位置‘1’。
对USART_TDR的写操作,或将USART_ICR的TCCF位置1。
TCIE
0: 发送未完成;1: 发送完成
USART_FLAG_RXNE
RXNE: 接收数据寄存器非空 (Read data register not empty)
当RDR移位寄存器中的数据被转移到USART_DR寄存器中,该位被硬件置位。
对USART_RDR的读操作,或将USART_RQR的RXFRQ位置1。
RXNEIE
0: 未收到数据;1: 收到数据可读出
串口FIFO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
typedef struct
{
USART_TypeDef * uart ; /* STM32内部串口设备指针 */
uint8_t * pTxBuf ; /* 发送缓冲区 */
uint8_t * pRxBuf ; /* 接收缓冲区 */
uint16_t usTxBufSize ; /* 发送缓冲区大小 */
uint16_t usRxBufSize ; /* 接收缓冲区大小 */
__IO uint16_t usTxWrite ; /* 发送缓冲区写指针 */
__IO uint16_t usTxRead ; /* 发送缓冲区读指针 */
__IO uint16_t usTxCount ; /* 等待发送的数据个数 */
__IO uint16_t usRxWrite ; /* 接收缓冲区写指针 */
__IO uint16_t usRxRead ; /* 接收缓冲区读指针 */
__IO uint16_t usRxCount ; /* 还未读取的新数据个数 */
void ( * SendBefor )( void ); /* 开始发送之前的回调函数指针(主要用于RS485切换到发送模式) */
void ( * SendOver )( void ); /* 发送完毕的回调函数指针(主要用于RS485将发送模式切换为接收模式) */
void ( * ReciveNew )( uint8_t _byte ); /* 串口收到数据的回调函数指针 */
uint8_t Sending ; /* 正在发送中 */
} UART_T ;
void bsp_InitUart ( void );
void comSendBuf ( COM_PORT_E _ucPort , uint8_t * _ucaBuf , uint16_t _usLen );
void comSendChar ( COM_PORT_E _ucPort , uint8_t _ucByte );
uint8_t comGetChar ( COM_PORT_E _ucPort , uint8_t * _pByte );
void comSendBuf ( COM_PORT_E _ucPort , uint8_t * _ucaBuf , uint16_t _usLen );
void comClearTxFifo ( COM_PORT_E _ucPort );
void comClearRxFifo ( COM_PORT_E _ucPort );
void comSetBaud ( COM_PORT_E _ucPort , uint32_t _BaudRate );
void USART_SetBaudRate ( USART_TypeDef * USARTx , uint32_t BaudRate );
void bsp_SetUartParam ( USART_TypeDef * Instance , uint32_t BaudRate , uint32_t Parity , uint32_t Mode );
void RS485_SendBuf ( uint8_t * _ucaBuf , uint16_t _usLen );
void RS485_SendStr ( char * _pBuf );
void RS485_SetBaud ( uint32_t _baud );
uint8_t UartTxEmpty ( COM_PORT_E _ucPort );
void comSendBuf ( COM_PORT_E _ucPort , uint8_t * _ucaBuf , uint16_t _usLen )
{
UART_T * pUart ;
pUart = ComToUart ( _ucPort );
if ( pUart == 0 )
{
return ;
}
if ( pUart -> SendBefor != 0 )
{
pUart -> SendBefor (); /* 如果是RS485通信,可以在这个函数中将RS485设置为发送模式 */
}
UartSend ( pUart , _ucaBuf , _usLen );
}
void comSendChar ( COM_PORT_E _ucPort , uint8_t _ucByte )
{
comSendBuf ( _ucPort , & _ucByte , 1 );
}
uint8_t comGetChar ( COM_PORT_E _ucPort , uint8_t * _pByte )
{
UART_T * pUart ;
pUart = ComToUart ( _ucPort );
if ( pUart == 0 )
{
return 0 ;
}
return UartGetChar ( pUart , _pByte );
}
void comClearTxFifo ( COM_PORT_E _ucPort )
{
UART_T * pUart ;
pUart = ComToUart ( _ucPort );
if ( pUart == 0 )
{
return ;
}
pUart -> usTxWrite = 0 ;
pUart -> usTxRead = 0 ;
pUart -> usTxCount = 0 ;
}
void comClearRxFifo ( COM_PORT_E _ucPort )
{
UART_T * pUart ;
pUart = ComToUart ( _ucPort );
if ( pUart == 0 )
{
return ;
}
pUart -> usRxWrite = 0 ;
pUart -> usRxRead = 0 ;
pUart -> usRxCount = 0 ;
}
void comSetBaud ( COM_PORT_E _ucPort , uint32_t _BaudRate )
{
USART_TypeDef * USARTx ;
USARTx = ComToUSARTx ( _ucPort );
if ( USARTx == 0 )
{
return ;
}
bsp_SetUartParam ( USARTx , _BaudRate , UART_PARITY_NONE , UART_MODE_TX_RX );
}
/* 如果是RS485通信,请按如下格式编写函数, 我们仅举了 USART3作为RS485的例子 */
void RS485_InitTXE ( void )
{
GPIO_InitTypeDef gpio_init ;
/* 打开GPIO时钟 */
RS485_TXEN_GPIO_CLK_ENABLE ();
/* 配置引脚为推挽输出 */
gpio_init . Mode = GPIO_MODE_OUTPUT_PP ; /* 推挽输出 */
gpio_init . Pull = GPIO_NOPULL ; /* 上下拉电阻不使能 */
gpio_init . Speed = GPIO_SPEED_FREQ_VERY_HIGH ; /* GPIO速度等级 */
gpio_init . Pin = RS485_TXEN_PIN ;
HAL_GPIO_Init ( RS485_TXEN_GPIO_PORT , & gpio_init );
}
void RS485_SetBaud ( uint32_t _baud )
{
comSetBaud ( COM3 , _baud );
}
void RS485_SendBefor ( void )
{
RS485_TX_EN (); /* 切换RS485收发芯片为发送模式 */
}
void RS485_SendOver ( void )
{
RS485_RX_EN (); /* 切换RS485收发芯片为接收模式 */
}
void RS485_SendBuf ( uint8_t * _ucaBuf , uint16_t _usLen )
{
comSendBuf ( COM3 , _ucaBuf , _usLen );
}
static void UartIRQ ( UART_T * _pUart )
{
uint32_t isrflags = READ_REG ( _pUart -> uart -> ISR );
uint32_t cr1its = READ_REG ( _pUart -> uart -> CR1 );
uint32_t cr3its = READ_REG ( _pUart -> uart -> CR3 );
/* 处理接收中断 */
if (( isrflags & USART_ISR_RXNE ) != RESET )
{
/* 从串口接收数据寄存器读取数据存放到接收FIFO */
uint8_t ch ;
ch = READ_REG ( _pUart -> uart -> RDR );
_pUart -> pRxBuf [ _pUart -> usRxWrite ] = ch ;
if ( ++ _pUart -> usRxWrite >= _pUart -> usRxBufSize )
{
_pUart -> usRxWrite = 0 ;
}
if ( _pUart -> usRxCount < _pUart -> usRxBufSize )
{
_pUart -> usRxCount ++ ;
}
/* 回调函数,通知应用程序收到新数据,一般是发送1个消息或者设置一个标记 */
//if (_pUart->usRxWrite == _pUart->usRxRead)
//if (_pUart->usRxCount == 1)
{
if ( _pUart -> ReciveNew )
{
_pUart -> ReciveNew ( ch ); /* 比如,交给MODBUS解码程序处理字节流 */
}
}
}
/* 处理发送缓冲区空中断 */
if ( (( isrflags & USART_ISR_TXE ) != RESET ) && ( cr1its & USART_CR1_TXEIE ) != RESET )
{
//if (_pUart->usTxRead == _pUart->usTxWrite)
if ( _pUart -> usTxCount == 0 )
{
/* 发送缓冲区的数据已取完时, 禁止发送缓冲区空中断 (注意:此时最后1个数据还未真正发送完毕)*/
//USART_ITConfig(_pUart->uart, USART_IT_TXE, DISABLE);
CLEAR_BIT ( _pUart -> uart -> CR1 , USART_CR1_TXEIE );
/* 使能数据发送完毕中断 */
//USART_ITConfig(_pUart->uart, USART_IT_TC, ENABLE);
SET_BIT ( _pUart -> uart -> CR1 , USART_CR1_TCIE );
}
else
{
_pUart -> Sending = 1 ;
/* 从发送FIFO取1个字节写入串口发送数据寄存器 */
//USART_SendData(_pUart->uart, _pUart->pTxBuf[_pUart->usTxRead]);
_pUart -> uart -> TDR = _pUart -> pTxBuf [ _pUart -> usTxRead ];
if ( ++ _pUart -> usTxRead >= _pUart -> usTxBufSize )
{
_pUart -> usTxRead = 0 ;
}
_pUart -> usTxCount -- ;
}
}
/* 数据bit位全部发送完毕的中断 */
if ((( isrflags & USART_ISR_TC ) != RESET ) && (( cr1its & USART_CR1_TCIE ) != RESET ))
{
//if (_pUart->usTxRead == _pUart->usTxWrite)
if ( _pUart -> usTxCount == 0 )
{
/* 如果发送FIFO的数据全部发送完毕,禁止数据发送完毕中断 */
//USART_ITConfig(_pUart->uart, USART_IT_TC, DISABLE);
CLEAR_BIT ( _pUart -> uart -> CR1 , USART_CR1_TCIE );
/* 回调函数, 一般用来处理RS485通信,将RS485芯片设置为接收模式,避免抢占总线 */
if ( _pUart -> SendOver )
{
_pUart -> SendOver ();
}
_pUart -> Sending = 0 ;
}
else
{
/* 正常情况下,不会进入此分支 */
/* 如果发送FIFO的数据还未完毕,则从发送FIFO取1个数据写入发送数据寄存器 */
//USART_SendData(_pUart->uart, _pUart->pTxBuf[_pUart->usTxRead]);
_pUart -> uart -> TDR = _pUart -> pTxBuf [ _pUart -> usTxRead ];
if ( ++ _pUart -> usTxRead >= _pUart -> usTxBufSize )
{
_pUart -> usTxRead = 0 ;
}
_pUart -> usTxCount -- ;
}
}
/* 清除中断标志 */
SET_BIT ( _pUart -> uart -> ICR , UART_CLEAR_PEF );
SET_BIT ( _pUart -> uart -> ICR , UART_CLEAR_FEF );
SET_BIT ( _pUart -> uart -> ICR , UART_CLEAR_NEF );
SET_BIT ( _pUart -> uart -> ICR , UART_CLEAR_OREF );
SET_BIT ( _pUart -> uart -> ICR , UART_CLEAR_IDLEF );
SET_BIT ( _pUart -> uart -> ICR , UART_CLEAR_TCF );
SET_BIT ( _pUart -> uart -> ICR , UART_CLEAR_LBDF );
SET_BIT ( _pUart -> uart -> ICR , UART_CLEAR_CTSF );
SET_BIT ( _pUart -> uart -> ICR , UART_CLEAR_CMF );
SET_BIT ( _pUart -> uart -> ICR , UART_CLEAR_WUF );
SET_BIT ( _pUart -> uart -> ICR , UART_CLEAR_TXFECF );
}
static void UartSend ( UART_T * _pUart , uint8_t * _ucaBuf , uint16_t _usLen )
{
uint16_t i ;
for ( i = 0 ; i < _usLen ; i ++ )
{
/* 如果发送缓冲区已经满了,则等待缓冲区空 */
while ( 1 )
{
__IO uint16_t usCount ;
DISABLE_INT ();
usCount = _pUart -> usTxCount ;
ENABLE_INT ();
if ( usCount < _pUart -> usTxBufSize )
{
break ;
}
else if ( usCount == _pUart -> usTxBufSize ) /* 数据已填满缓冲区 */
{
if (( _pUart -> uart -> CR1 & USART_CR1_TXEIE ) == 0 )
{
SET_BIT ( _pUart -> uart -> CR1 , USART_CR1_TXEIE );
}
}
}
/* 将新数据填入发送缓冲区 */
_pUart -> pTxBuf [ _pUart -> usTxWrite ] = _ucaBuf [ i ];
DISABLE_INT ();
if ( ++ _pUart -> usTxWrite >= _pUart -> usTxBufSize )
{
_pUart -> usTxWrite = 0 ;
}
_pUart -> usTxCount ++ ;
ENABLE_INT ();
}
SET_BIT ( _pUart -> uart -> CR1 , USART_CR1_TXEIE ); /* 使能发送中断(缓冲区空) */
}
static uint8_t UartGetChar ( UART_T * _pUart , uint8_t * _pByte )
{
uint16_t usCount ;
/* usRxWrite 变量在中断函数中被改写,主程序读取该变量时,必须进行临界区保护 */
DISABLE_INT ();
usCount = _pUart -> usRxCount ;
ENABLE_INT ();
/* 如果读和写索引相同,则返回0 */
//if (_pUart->usRxRead == usRxWrite)
if ( usCount == 0 ) /* 已经没有数据 */
{
return 0 ;
}
else
{
* _pByte = _pUart -> pRxBuf [ _pUart -> usRxRead ]; /* 从串口接收FIFO取1个数据 */
/* 改写FIFO读索引 */
DISABLE_INT ();
if ( ++ _pUart -> usRxRead >= _pUart -> usRxBufSize )
{
_pUart -> usRxRead = 0 ;
}
_pUart -> usRxCount -- ;
ENABLE_INT ();
return 1 ;
}
}
低功耗串口 LPUART
TIM 定时器
时基单元
预分频器寄存器 (TIMx_PSC)用于设置定时器的分频,比如定时器的主频是 200MHz,通过此寄存器可以将其设置为 100MHz,50MHz, 25MHz 等分频值。注: 预分频器有个缓冲功能,可以让用户实时更改,新的预分频值将在下一个更新事件发生时被采用(以递增计数模式为例,就是 CNT 计数值达到 ARR 自动重装寄存器的数值时会产生更新事件)。
计数器寄存器 (TIMx_CNT)计数器是最基本的计数单元, 计数值是建立在分频的基础上面,比如通过 TIMx_PSC 设置分频后的频率为 100MHz,那么计数寄存器计一次数就是 10ns。
自动重载寄存器 (TIMx_ARR)自动重装寄存器是 CNT 计数寄存器能达到的最大计数值, 以递增计数模式为例,就是 CNT 计数器达到 ARR 寄存器数值时, 重新从 0 开始计数。注, 自动重载寄存器是预装载的。对自动重载寄存器执行写入或读取操作时会访问预装载寄存器。预装载寄存器的内容既可以立即传送到影子寄存器(让设置立即起到效果的寄存器) ,也可以在每次发生更新事件时传送到影子寄存器。简单的说就是让 ARR 寄存器的数值立即更新还是更新事件发送的时候更新。
重复计数器寄存器 (TIMx_RCR)以递增计数模式为例,当 CNT 计数器数值达到 ARR 自动重载数值时,重复计数器的数值加 1,重复次数达到 TIMx_RCR+ 1 后就,将生成更新事件。注, 只有 TIM1, TIM8, TIM15, TIM16, TIM17 有此寄存器.
定时器输出比较(PWM)
以 PWM 边沿对齐模式,递增计数配置为例:
当计数器 TIMx_CNT < 比较捕获寄存器 TIMx_CCRx 期间, PWM 参考信号 OCxREF 输出高电平。
当计数器 TIMx_CNT >= 比较捕获寄存器 TIMx_CCRx 期间, PWM 参考信号 OCxREF 输出低电平。
当比较捕获寄存器 TIMx_CCRx > 自动重载寄存器 TIMx_ARR, OCxREF 保持为 1。
当比较捕获寄存器 TIMx_CCRx = 0,则 OCxRef 保持为 0。
定时器输入捕获
使用定时器实现输入捕获,仅靠时基单元的那几个寄存器是不行的,我们需要一个寄存器来记录发生捕获时的具体时间, 这个寄存器依然由比较捕获寄存器 TIMx_CCRx 来实现。
配置定时器为输入捕获模式, 上升沿触发,设置分频, 自动重装等寄存器, 比如设置的 CNT 计数器计数 1 次是 1 微秒。
当有上升沿触发的时候, TIMx_CCRx 寄存器就会自动记录当前的 CNT 数值, 然后用户就可以通过CC 中断,在中断复位程序里面保存当前的 TIMx_CCRx 寄存器数值。 等下次再检测到上升沿触发, 两次时间求差就可以得到方波的周期。
不过这里要特别注意一点, 如果 CNT 发生溢出(比如 16 位定时器,计数到 65535 就溢出了)就需要特别处理下,将 CNT 计数溢出考虑进来
PWM
$$
_ulFreq=\frac{f_{sys}}{(ARR+1)(PSC+1)}
$$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
输出比较模式
// 使能 | 失能TIM RCC时钟
_HAL_RCC_TIM1_CLK_ENABLE ();
__HAL_RCC_TIM3_CLK_DISABLE ();
void bsp_ConfigTimGpio ( GPIO_TypeDef * GPIOx , uint16_t GPIO_PinX , TIM_TypeDef * TIMx )
{
GPIO_InitTypeDef GPIO_InitStruct ;
/* 使能GPIO时钟 */
bsp_RCC_GPIO_Enable ( GPIOx );
/* 使能TIM时钟 */
bsp_RCC_TIM_Enable ( TIMx );
GPIO_InitStruct . Mode = GPIO_MODE_AF_PP ;
GPIO_InitStruct . Pull = GPIO_PULLUP ;
GPIO_InitStruct . Speed = GPIO_SPEED_FREQ_VERY_HIGH ;
GPIO_InitStruct . Alternate = bsp_GetAFofTIM ( TIMx );
GPIO_InitStruct . Pin = GPIO_PinX ;
HAL_GPIO_Init ( GPIOx , & GPIO_InitStruct );
}
//配置GPIO为推挽输出。主要用于PWM输出,占空比为0和100的情况。
void bsp_ConfigGpioOut ( GPIO_TypeDef * GPIOx , uint16_t GPIO_PinX )
{
GPIO_InitTypeDef GPIO_InitStruct ;
bsp_RCC_GPIO_Enable ( GPIOx ); /* 使能GPIO时钟 */
GPIO_InitStruct . Mode = GPIO_MODE_OUTPUT_PP ;
GPIO_InitStruct . Pull = GPIO_NOPULL ;
GPIO_InitStruct . Speed = GPIO_SPEED_FREQ_VERY_HIGH ;
GPIO_InitStruct . Pin = GPIO_PinX ;
HAL_GPIO_Init ( GPIOx , & GPIO_InitStruct );
}
//_ulFreq : PWM信号频率,单位Hz (实际测试,可以输出100MHz),0 表示禁止输出
//_ulDutyCycle : PWM信号占空比,单位: 万分之一。如5000,表示50.00%的占空比
void bsp_SetTIMOutPWM ( GPIO_TypeDef * GPIOx , uint16_t GPIO_Pin , TIM_TypeDef * TIMx , uint8_t _ucChannel , uint32_t _ulFreq , uint32_t _ulDutyCycle )
{
TIM_HandleTypeDef TimHandle = { 0 };
TIM_OC_InitTypeDef sConfig = { 0 };
uint16_t usPeriod ;
uint16_t usPrescaler ;
uint32_t pulse ;
uint32_t uiTIMxCLK ;
const uint16_t TimChannel [ 6 + 1 ] = { 0 , TIM_CHANNEL_1 , TIM_CHANNEL_2 , TIM_CHANNEL_3 , TIM_CHANNEL_4 , TIM_CHANNEL_5 , TIM_CHANNEL_6 };
if ( _ucChannel > 6 )
{
Error_Handler ( __FILE__ , __LINE__ );
}
if ( _ulDutyCycle == 0 )
{
//bsp_RCC_TIM_Disable(TIMx); /* 关闭TIM时钟, 可能影响其他通道 */
bsp_ConfigGpioOut ( GPIOx , GPIO_Pin ); /* 配置GPIO为推挽输出 */
GPIOx -> BSRRH = GPIO_Pin ; /* PWM = 0 */
return ;
}
else if ( _ulDutyCycle == 10000 )
{
//bsp_RCC_TIM_Disable(TIMx); /* 关闭TIM时钟, 可能影响其他通道 */
bsp_ConfigGpioOut ( GPIOx , GPIO_Pin ); /* 配置GPIO为推挽输出 */
GPIOx -> BSRRL = GPIO_Pin ; /* PWM = 1*/
return ;
}
/* 下面是PWM输出 */
bsp_ConfigTimGpio ( GPIOx , GPIO_Pin , TIMx ); /* 使能GPIO和TIM时钟,并连接TIM通道到GPIO */
/*-----------------------------------------------------------------------
bsp.c 文件中 void SystemClock_Config(void) 函数对时钟的配置如下:
System Clock source = PLL (HSE)
SYSCLK(Hz) = 400000000 (CPU Clock)
HCLK(Hz) = 200000000 (AXI and AHBs Clock)
AHB Prescaler = 2
D1 APB3 Prescaler = 2 (APB3 Clock 100MHz)
D2 APB1 Prescaler = 2 (APB1 Clock 100MHz)
D2 APB2 Prescaler = 2 (APB2 Clock 100MHz)
D3 APB4 Prescaler = 2 (APB4 Clock 100MHz)
因为APB1 prescaler != 1, 所以 APB1上的TIMxCLK = APB1 x 2 = 200MHz;
因为APB2 prescaler != 1, 所以 APB2上的TIMxCLK = APB2 x 2 = 200MHz;
APB4上面的TIMxCLK没有分频,所以就是100MHz;
APB1 定时器有 TIM2, TIM3 ,TIM4, TIM5, TIM6, TIM7, TIM12, TIM13, TIM14,LPTIM1
APB2 定时器有 TIM1, TIM8 , TIM15, TIM16,TIM17
APB4 定时器有 LPTIM2,LPTIM3,LPTIM4,LPTIM5
----------------------------------------------------------------------- */
if (( TIMx == TIM1 ) || ( TIMx == TIM8 ) || ( TIMx == TIM15 ) || ( TIMx == TIM16 ) || ( TIMx == TIM17 ))
{
/* APB2 定时器时钟 = 200M */
uiTIMxCLK = SystemCoreClock / 2 ;
}
else
{
/* APB1 定时器 = 200M */
uiTIMxCLK = SystemCoreClock / 2 ;
}
if ( _ulFreq < 100 )
{
usPrescaler = 10000 - 1 ; /* 分频比 = 10000 */
usPeriod = ( uiTIMxCLK / 10000 ) / _ulFreq - 1 ; /* 自动重装的值 */
}
else if ( _ulFreq < 3000 )
{
usPrescaler = 100 - 1 ; /* 分频比 = 100 */
usPeriod = ( uiTIMxCLK / 100 ) / _ulFreq - 1 ; /* 自动重装的值 */
}
else /* 大于4K的频率,无需分频 */
{
usPrescaler = 0 ; /* 分频比 = 1 */
usPeriod = uiTIMxCLK / _ulFreq - 1 ; /* 自动重装的值 */
}
pulse = ( _ulDutyCycle * usPeriod ) / 10000 ;
HAL_TIM_PWM_DeInit ( & TimHandle );
/* PWM频率 = TIMxCLK / usPrescaler + 1)/usPeriod + 1)*/
TimHandle . Instance = TIMx ;
TimHandle . Init . Prescaler = usPrescaler ;
TimHandle . Init . Period = usPeriod ;
TimHandle . Init . ClockDivision = 0 ;
TimHandle . Init . CounterMode = TIM_COUNTERMODE_UP ;
TimHandle . Init . RepetitionCounter = 0 ;
TimHandle . Init . AutoReloadPreload = 0 ;
if ( HAL_TIM_PWM_Init ( & TimHandle ) != HAL_OK )
{
Error_Handler ( __FILE__ , __LINE__ );
}
/* 配置定时器PWM输出通道 */
sConfig . OCMode = TIM_OCMODE_PWM1 ;
sConfig . OCPolarity = TIM_OCPOLARITY_HIGH ;
sConfig . OCFastMode = TIM_OCFAST_DISABLE ;
sConfig . OCNPolarity = TIM_OCNPOLARITY_HIGH ;
sConfig . OCNIdleState = TIM_OCNIDLESTATE_RESET ;
sConfig . OCIdleState = TIM_OCIDLESTATE_RESET ;
/* 占空比 */
sConfig . Pulse = pulse ;
if ( HAL_TIM_PWM_ConfigChannel ( & TimHandle , & sConfig , TimChannel [ _ucChannel ]) != HAL_OK )
{
Error_Handler ( __FILE__ , __LINE__ );
}
/* 启动PWM输出 */
if ( HAL_TIM_PWM_Start ( & TimHandle , TimChannel [ _ucChannel ]) != HAL_OK )
{
Error_Handler ( __FILE__ , __LINE__ );
}
}
高精度单次延迟实现
1
2
3
4
5
TL ; DR
配置捕获比较中断
读取当前 cnt , 设置为 CCR = CNT + WAIT_TICKS_YOU_WANT
TIMx -> SR = ( uint16_t ) ~ TIM_IT_CC1 ; /* 清除 CC1 中断标志 */
TIMx -> DIER |= TIM_IT_CC1 ; /* 使能 CC1 中断 */
LPTIM 低功耗定时器基础
LPTIM 的好处是系统处于睡眠,停机状态依然可以正常工作,但停机模式不能再正常工作。
对于睡眠模式, 任何受 NVIC 控制的中断都可以唤醒休眠模式。 进入睡眠模式 调用函数HAL_PWR_EnterSLEEPMode 即可。
在系统停止模式下, 1.2V 供电域中的所有时钟都停止, PLL, HSI 和 HSE RC 振荡器被禁用。内部 SRAM和寄存器内容保留。 而 LSE 和 LSI 是可以正常工作的, 所以 LPTIM 系统时钟使用 LSE 或者 LSI 依然可以在停机模式下工作。进入停机模式调用函数 HAL_PWR_EnterSTOPMode 即可。
LPTIM PWM
差不多
LPTIM 超时唤醒
注意
LPTIM 的任何中断都可以唤醒停机模式
STM32H7 从停机模式唤醒后要重新配置系统时钟,这点跟 F1, F4 系列一样。
测试发现 STM32H7 的 LPTIM1 的中断可以唤醒停机模式,其它几个 LPTIM2-5 无法唤醒。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
void bsp_InitLPTIM ( void )
{
RCC_PeriphCLKInitTypeDef RCC_PeriphCLKInitStruct = { 0 };
/* ## - 1 - 使能LPTIM时钟和GPIO时钟 ####################################### */
__HAL_RCC_LPTIM1_CLK_ENABLE ();
/* ## - 2 - 配置LPTIM时钟,可以选择LSE,LSI或者PCLK ######################## */
#if defined (LPTIM_CLOCK_SOURCE_LSE)
{
RCC_OscInitTypeDef RCC_OscInitStruct = { 0 };
RCC_OscInitStruct . OscillatorType = RCC_OSCILLATORTYPE_LSE ;
RCC_OscInitStruct . LSEState = RCC_LSE_ON ;
RCC_OscInitStruct . PLL . PLLState = RCC_PLL_NONE ;
if ( HAL_RCC_OscConfig ( & RCC_OscInitStruct ) != HAL_OK )
{
Error_Handler ( __FILE__ , __LINE__ );
}
RCC_PeriphCLKInitStruct . PeriphClockSelection = RCC_PERIPHCLK_LPTIM1 ;
RCC_PeriphCLKInitStruct . Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE ;
HAL_RCCEx_PeriphCLKConfig ( & RCC_PeriphCLKInitStruct );
}
#elif defined (LPTIM_CLOCK_SOURCE_LSI)
{
RCC_OscInitTypeDef RCC_OscInitStruct = { 0 };
RCC_OscInitStruct . OscillatorType = RCC_OSCILLATORTYPE_LSI ;
RCC_OscInitStruct . LSIState = RCC_LSI_ON ;
RCC_OscInitStruct . PLL . PLLState = RCC_PLL_NONE ;
if ( HAL_RCC_OscConfig ( & RCC_OscInitStruct ) != HAL_OK )
{
Error_Handler ( __FILE__ , __LINE__ );
}
RCC_PeriphCLKInitStruct . PeriphClockSelection = RCC_PERIPHCLK_LPTIM1 ;
RCC_PeriphCLKInitStruct . Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSI ;
HAL_RCCEx_PeriphCLKConfig ( & RCC_PeriphCLKInitStruct );
}
#elif defined (LPTIM_CLOCK_SOURCE_PCLK)
/*-----------------------------------------------------------------------
bsp.c 文件中 void SystemClock_Config(void) 函数对时钟的配置如下:
System Clock source = PLL (HSE)
SYSCLK(Hz) = 400000000 (CPU Clock)
HCLK(Hz) = 200000000 (AXI and AHBs Clock)
AHB Prescaler = 2
D1 APB3 Prescaler = 2 (APB3 Clock 100MHz)
D2 APB1 Prescaler = 2 (APB1 Clock 100MHz)
D2 APB2 Prescaler = 2 (APB2 Clock 100MHz)
D3 APB4 Prescaler = 2 (APB4 Clock 100MHz)
因为APB1 prescaler != 1, 所以 APB1上的TIMxCLK = APB1 x 2 = 200MHz;
因为APB2 prescaler != 1, 所以 APB2上的TIMxCLK = APB2 x 2 = 200MHz;
APB4上面的TIMxCLK没有分频,所以就是100MHz;
APB1 定时器有 TIM2, TIM3 ,TIM4, TIM5, TIM6, TIM7, TIM12, TIM13, TIM14,LPTIM1
APB2 定时器有 TIM1, TIM8 , TIM15, TIM16,TIM17
APB4 定时器有 LPTIM2,LPTIM3,LPTIM4,LPTIM5
----------------------------------------------------------------------- */
RCC_PeriphCLKInitStruct . PeriphClockSelection = RCC_PERIPHCLK_LPTIM1 ;
RCC_PeriphCLKInitStruct . Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE ;
HAL_RCCEx_PeriphCLKConfig ( & RCC_PeriphCLKInitStruct );
#else
#error Please select the LPTIM Clock source inside the bsp_lptim_pwm.c file
#endif
/* ## - 3 - 配置LPTIM ######################################################## */
LptimHandle . Instance = LPTIM1 ;
LptimHandle . Init . Clock . Source = LPTIM_CLOCKSOURCE_APBCLOCK_LPOSC ; /* 对应寄存器CKSEL,选择内部时钟源 */
LptimHandle . Init . Clock . Prescaler = LPTIM_PRESCALER_DIV8 ; /* 设置LPTIM时钟分频 */
LptimHandle . Init . CounterSource = LPTIM_COUNTERSOURCE_INTERNAL ; /* LPTIM计数器对内部时钟源计数 */
LptimHandle . Init . Trigger . Source = LPTIM_TRIGSOURCE_SOFTWARE ; /* 软件触发 */
LptimHandle . Init . OutputPolarity = LPTIM_OUTPUTPOLARITY_HIGH ; /* 超时模式用不到这个配置 */
LptimHandle . Init . UpdateMode = LPTIM_UPDATE_IMMEDIATE ; /* 比较寄存器和ARR自动重载寄存器选择更改后立即更新 */
LptimHandle . Init . Input1Source = LPTIM_INPUT1SOURCE_GPIO ; /* 外部输入1,本配置未使用 */
LptimHandle . Init . Input2Source = LPTIM_INPUT2SOURCE_GPIO ; /* 外部输入2,本配置未使用 */
if ( HAL_LPTIM_Init ( & LptimHandle ) != HAL_OK )
{
Error_Handler ( __FILE__ , __LINE__ );
}
/* ## - 4 - 配置LPTIM ######################################################## */
/* 配置中断优先级并使能中断 */
HAL_NVIC_SetPriority ( LPTIM1_IRQn , 1 , 0 );
HAL_NVIC_EnableIRQ ( LPTIM1_IRQn );
}
高分辨率定时器 HRTIM
HRTIM PWM
DMA
DMAMUX 基础知识
DMAMUX 其实就是 DMA 控制器前一级的多路选择器,有了这个选择器就不用再像 F1, F4 系列那样每个通道(数据流) 要固定选择指定的外设, 有了多路选择器就可以任意选择, 外设使用 DMA 方式时无需再选择指定的 DMA 通道(数据流) ,任意通道(数据流) 都可以。
当前STM32H7有两路DMAMUX, 分别是DMAMUX1和DMAMUX2,其中DMAMUX1负责DMA1和 DMA2,而 DMAMUX2 负责 BDMA。
DMA1, DMA2 和 BDMA 都支持存储区到存储区的传输。
Request Multiplexer(请求多路复用器)
将 某一 DMAMUX 通道 的输入,从 多个可能的 DMA 请求源 中选择一个。
多进一出,选一个通
Request Generator(请求发生器)
将任意外部事件(如 GPIO 边沿、定时器信号、LPTIM 等)转换成一个 “虚拟 DMA 请求”。
BDMA 基础知识
BDMA 只能操作 D3 域的存储器和外设 ,这点比较重要,操作的时候容易被遗忘。 详情看本章 2.6 小节。
BDMA 支持 8 路通道。 虽然是 8 路,但这 8 路不是并行工作的, 而是由 BDMA 的仲裁器决定当前处理哪一路。
BDMA 不支持硬件 FIFO,但是支持双缓冲。
BDMA 不支持突发模式。
BDMA 最大传输次数 65535 次, 每次传输单位可以是字节、 半字和字。
BDMA 的循环模式不可用于存储器到存储器模式
BDMA 支持存储器到外设,外设到存储器,存储器到存储器(循环模式不可用于存储器到存储器模式 )和外设到外设的传输,其中外设到外设的传输, DMA1 和 DMA2 是不支持的,这个模式在低功耗模式下比较有用。
BDMA 只有一个 AHB 总线主控,而 DMA1 和 DMA2 是有两个的,可以分别用于源地址和目的地址的传输。
源地址和目的地址的数据宽度可以不同,但是数据地址必须要跟其数据类型对齐。比如源地址是uint32 类型的,那么此数组的地址必须 4 字节对齐。
BDMA 主要有两种模式,一个是 Normal 正常模式,传输一次后就停止传输;另一种是 Circular 循环模式,会一直循环的传输下去,即使有 DMA 中断,传输也是一直在进行的。
BDMA 的通道请求(Channel0 – Channel7) 的优先级可编程, 分为四级 Very high priority, Highpriority, Medium priority 和 Low priority。 通道的优先级配置相同的情况下, 如果同时产生请求,会优先响应编号低的,即 Channel0 优先响应。
BDMA 双缓冲
当用户开启了 BDMA 传输完成中断后, 通过寄存器 CCRx 的 CT 位判断当前使用的是哪个缓冲区:
如果 CT = 1 表示当前正在使用缓冲区 1, 即寄存器 BDMA_CM1ARx 记录的地址。
如果 CT = 0 表示当前正在使用缓冲区 0, 即寄存器 BDMA_CM0ARx 记录的地址。
BDMA 仅可以操作: AHB4, APB4 的外设以及 SRAM4, Backup RAM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#if defined ( __ICCARM__ )
#pragma location = 0x38000000
uint32_t IO_Toggle [ 8 ] =
{
0x00000002U ,
0x00020000U ,
0x00000002U ,
0x00020000U ,
0x00000002U ,
0x00020000U ,
0x00000002U ,
0x00020000U ,
};
#elif defined ( __CC_ARM )
ALIGN_32BYTES ( __attribute__ (( section ( ".RAM_D3" ))) uint32_t IO_Toggle [ 8 ]) =
{
0x00000002U ,
0x00020000U ,
0x00000002U ,
0x00020000U ,
0x00000002U ,
0x00020000U ,
0x00000002U ,
0x00020000U ,
};
#endif
void bsp_InitTimBDMA ( void )
{
GPIO_InitTypeDef GPIO_InitStruct ;
DMA_HandleTypeDef DMA_Handle = { 0 };
HAL_DMA_MuxRequestGeneratorConfigTypeDef dmamux_ReqGenParams = { 0 };
/*##-1- 配置PB1用于PWM输出######################################*/
__HAL_RCC_GPIOB_CLK_ENABLE ();
GPIO_InitStruct . Pin = GPIO_PIN_1 ;
GPIO_InitStruct . Mode = GPIO_MODE_OUTPUT_PP ;
GPIO_InitStruct . Pull = GPIO_NOPULL ;
GPIO_InitStruct . Speed = GPIO_SPEED_FREQ_VERY_HIGH ;
HAL_GPIO_Init ( GPIOB , & GPIO_InitStruct );
/*##-2- 配置DMA ##################################################*/
__HAL_RCC_BDMA_CLK_ENABLE ();
DMA_Handle . Instance = BDMA_Channel0 ; /* 使用的BDMA通道0 */
DMA_Handle . Init . Request = BDMA_REQUEST_GENERATOR0 ; /* 请求类型采用的DMAMUX请求发生器通道0 */
DMA_Handle . Init . Direction = DMA_MEMORY_TO_PERIPH ; /* 传输方向是从存储器到外设 */
DMA_Handle . Init . PeriphInc = DMA_PINC_DISABLE ; /* 外设地址自增禁止 */
DMA_Handle . Init . MemInc = DMA_MINC_ENABLE ; /* 存储器地址自增使能 */
DMA_Handle . Init . PeriphDataAlignment = DMA_PDATAALIGN_WORD ; /* 外设数据传输位宽选择字,即32bit */
DMA_Handle . Init . MemDataAlignment = DMA_MDATAALIGN_WORD ; /* 存储器数据传输位宽选择字,即32bit */
DMA_Handle . Init . Mode = DMA_CIRCULAR ; /* 循环模式 */
DMA_Handle . Init . Priority = DMA_PRIORITY_LOW ; /* 优先级低 */
DMA_Handle . Init . FIFOMode = DMA_FIFOMODE_DISABLE ; /* BDMA不支持FIFO */
DMA_Handle . Init . FIFOThreshold = DMA_FIFO_THRESHOLD_FULL ; /* BDMA不支持FIFO阀值设置 */
DMA_Handle . Init . MemBurst = DMA_MBURST_SINGLE ; /* BDMA不支持存储器突发 */
DMA_Handle . Init . PeriphBurst = DMA_PBURST_SINGLE ; /* BDMA不支持外设突发 */
HAL_DMA_Init ( & DMA_Handle );
/* 开启BDMA Channel0的中断 */
HAL_NVIC_SetPriority ( BDMA_Channel0_IRQn , 2 , 0 );
HAL_NVIC_EnableIRQ ( BDMA_Channel0_IRQn );
/*##-3- 配置DMAMUX #########################################################*/
dmamux_ReqGenParams . SignalID = HAL_DMAMUX2_REQ_GEN_LPTIM2_OUT ; /* 请求触发器选择LPTIM2_OUT */
dmamux_ReqGenParams . Polarity = HAL_DMAMUX_REQ_GEN_RISING_FALLING ; /* LPTIM2输出的上升沿和下降沿均可触发 */
dmamux_ReqGenParams . RequestNumber = 1 ; /* 触发后,传输进行1次DMA传输 */
HAL_DMAEx_ConfigMuxRequestGenerator ( & DMA_Handle , & dmamux_ReqGenParams ); /* 配置DMAMUX */
HAL_DMAEx_EnableMuxRequestGenerator ( & DMA_Handle ); /* 使能DMAMUX请求发生器 */
/*##-4- 启动DMA传输 ################################################*/
HAL_DMA_Start_IT ( & DMA_Handle , ( uint32_t ) IO_Toggle , ( uint32_t ) & GPIOB -> BSRRL , 8 );
/*
默认情况下,用户通过注册回调函数DMA_Handle.XferHalfCpltCallback,然后函数HAL_DMA_Init会开启半传输完成中断,
由于这里没有使用HAL库默认的中断管理函数HAL_DMA_IRQHandler,直接手动开启。
*/
BDMA_Channel0 -> CCR |= BDMA_CCR_HTIE ;
LPTIM_Config (); /* 配置LPTIM触发DMAMUX */
}
void BDMA_Channel0_IRQHandler ( void )
{
/* 传输完成中断 */
if (( BDMA -> ISR & BDMA_FLAG_TC0 ) != RESET )
{
BDMA -> IFCR = BDMA_FLAG_TC0 ;
/*
1、传输完成开始使用DMA缓冲区的前半部分,此时可以动态修改后半部分数据
比如缓冲区大小是IO_Toggle[0] 到 IO_Toggle[7]
那么此时可以修改IO_Toggle[4] 到 IO_Toggle[7]
2、变量所在的SRAM区已经通过MPU配置为WT模式,更新变量IO_Toggle会立即写入。
3、不配置MPU的话,也可以通过Cahce的函数SCB_CleanDCache_by_Addr做Clean操作。
*/
}
/* 半传输完成中断 */
if (( BDMA -> ISR & BDMA_FLAG_HT0 ) != RESET )
{
BDMA -> IFCR = BDMA_FLAG_HT0 ;
/*
1、半传输完成开始使用DMA缓冲区的后半部分,此时可以动态修改前半部分数据
比如缓冲区大小是IO_Toggle[0] 到 IO_Toggle[7]
那么此时可以修改IO_Toggle[0] 到 IO_Toggle[3]
2、变量所在的SRAM区已经通过MPU配置为WT模式,更新变量IO_Toggle会立即写入。
3、不配置MPU的话,也可以通过Cahce的函数SCB_CleanDCache_by_Addr做Clean操作。
*/
}
/* 传输错误中断 */
if (( BDMA -> ISR & BDMA_FLAG_TE0 ) != RESET )
{
BDMA -> IFCR = BDMA_FLAG_TE0 ;
}
}
BDMA 应用之控制任意 IO做 PWM 和脉冲数控制
就是上面的代码,配置tim上升沿下降沿都发生中断,将一个数组内的数字分别传入GPIOB->BSRRL,实现PWM
动态修改需要注意的点
方法一:
设置 BDMA 所使用 SRAM3 存储区的 Cache 属性为 Write through, read allocate, no writeallocate。保证写入的数据会立即更新到 SRAM3 里面
方法二:
设置 SRAM3 的 缓冲区做 32 字节对齐,大小最好也是 32 字节整 数倍 , 然后 调用函数SCB_CleanDCache_by_Addr 做 Clean 操作即可,保证 BDMA 读取到的数据是刚更新好的。
DMA 基础知识
DMA1 和 DMA2 均支持 8 路通道。 虽然是 8 路,但这 8 路不是并行工作的, 而是由 DMA 的仲裁器决定当前处理那一路。
DMA 最大传输次数 65535 次, 每次传输单位可以是字节、 半字和字。
DMA 的循环模式不可用于存储器到存储器模式。
DMA1 和 DMA2 带的 FIFO 是 4 个 32bit 的空间,即 16 字节。
使用 DMA 的 FIFO 和突发需要注意的问题较多
关键知识点
由于总线矩阵的存在,各个主控的道路四通八达,从而可以让 DMA 和 CPU 同时开工,但是注意一点,如果他们同时访问的同一个外设,会有一点性能影响的。
DMA 支持存储器到外设,外设到存储器和存储器到存储器的传输, 不支持外设到外设的传输, 而BDMA 是支持的,这个模式在低功耗模式下比较有用。
DMA1 和 DMA2 是有两个 AHB 总线主控,可以分别用于源地址和目的地址的传输。
源地址和目的地址的数据宽度可以不同,但是数据地址必须要跟其数据类型对齐。比如源地址是uint32 类型的,那么此数组的地址必须 4 字节对齐。
DMA 主要有两种模式,一个是 Normal 正常模式,传输一次后就停止传输;另一种是 Circular 循环模式,会一直循环的传输下去,即使有 DMA 中断,传输也是一直在进行的。
DMA 的数据流请求(Stream0 – Stream7) 的优先级可编程, 分为四级 Very high priority, High priority, Medium priority 和 Low priority。 通道的优先级配置相同的情况下, 如果同时产生请求,会优先响应编号低的,即 Stream0 优先响应。
DMA 的 FIFO 和突发支持
使用 DMA 的 FIFO 主要有两个作用,一个是降低总线带宽的需求,另一个是前面说的源地址数据宽度和目的地址数据宽度不同时的数据传输。而突发传输的含义是每个 DMA 请求后可以连续传输的数据项目数, 支持 4 次, 8 次和 16 次。了解到以上两点就够用了,现在重点讲解下使用中的注意事项,使用 FIFO 要注意的事项较多。
禁止 FIFO 的情况下,即 STM32H7 参考手册里面所说的直接模式 Direct Mode,务必要保证外设数据宽度和内存数据宽度是一样的,而且禁止了 FIFO 的情况下, 不支持突发,即使配置了,也是无效的。
禁止了 FIFO 的情况下, 也不可用于存储器到存储器的数据传输, 仅支持外设到存储器或者存储器到外设方式。
使能 FIFO 的情况下, 可以使用突发模式, 也可以不使用。
独立的源和目标传输宽度(字节、半字、字):源和目标的数据宽度不相等时, DMA 自动封装/解封必要的传输数据来优化带宽。这个特性仅在 FIFO 模式下可用。这个特性非常重要,在 H7 使用SDIO 时要用到。无需像 F1 系列那样强行数据缓冲的 4 字节对齐要求。
最后要特别注意一点,一般应用中最好关闭 FIFO, 实际测试发现容易有一些比较奇怪的问题.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// 多缓冲区的DMA的启用函数
HAL_DMAEx_MultiBufferStart ()
void bsp_InitTimDMA ( void )
{
GPIO_InitTypeDef GPIO_InitStruct ;
DMA_HandleTypeDef DMA_Handle = { 0 };
HAL_DMA_MuxRequestGeneratorConfigTypeDef dmamux_ReqGenParams = { 0 };
/*##-1- 配置PB1用于PWM输出 ##################################################*/
__HAL_RCC_GPIOB_CLK_ENABLE ();
GPIO_InitStruct . Pin = GPIO_PIN_1 ;
GPIO_InitStruct . Mode = GPIO_MODE_OUTPUT_PP ;
GPIO_InitStruct . Pull = GPIO_NOPULL ;
GPIO_InitStruct . Speed = GPIO_SPEED_FREQ_VERY_HIGH ;
HAL_GPIO_Init ( GPIOB , & GPIO_InitStruct );
/*##-2- 使能DMA1时钟并配置 ##################################################*/
__HAL_RCC_DMA1_CLK_ENABLE ();
DMA_Handle . Instance = DMA1_Stream1 ; /* 使用的DMA1 Stream1 */
DMA_Handle . Init . Request = DMA_REQUEST_GENERATOR0 ; /* 请求类型采用的DMAMUX请求发生器通道0 */
DMA_Handle . Init . Direction = DMA_MEMORY_TO_PERIPH ; /* 传输方向是从存储器到外设 */
DMA_Handle . Init . PeriphInc = DMA_PINC_DISABLE ; /* 外设地址自增禁止 */
DMA_Handle . Init . MemInc = DMA_MINC_ENABLE ; /* 存储器地址自增使能 */
DMA_Handle . Init . PeriphDataAlignment = DMA_PDATAALIGN_WORD ; /* 外设数据传输位宽选择字,即32bit */
DMA_Handle . Init . MemDataAlignment = DMA_MDATAALIGN_WORD ; /* 存储器数据传输位宽选择字,即32bit */
DMA_Handle . Init . Mode = DMA_CIRCULAR ; /* 循环模式 */
DMA_Handle . Init . Priority = DMA_PRIORITY_LOW ; /* 优先级低 */
DMA_Handle . Init . FIFOMode = DMA_FIFOMODE_DISABLE ; /* 禁止FIFO*/
DMA_Handle . Init . FIFOThreshold = DMA_FIFO_THRESHOLD_FULL ; /* 禁止FIFO此位不起作用,用于设置阀值 */
DMA_Handle . Init . MemBurst = DMA_MBURST_SINGLE ; /* 禁止FIFO此位不起作用,用于存储器突发 */
DMA_Handle . Init . PeriphBurst = DMA_PBURST_SINGLE ; /* 禁止FIFO此位不起作用,用于外设突发 */
/* 初始化DMA */
if ( HAL_DMA_Init ( & DMA_Handle ) != HAL_OK )
{
Error_Handler ( __FILE__ , __LINE__ );
}
/* 开启DMA1 Stream1的中断 */
HAL_NVIC_SetPriority ( DMA1_Stream1_IRQn , 2 , 0 );
HAL_NVIC_EnableIRQ ( DMA1_Stream1_IRQn );
/*##-4- 配置DMAMUX ###########################################################*/
dmamux_ReqGenParams . SignalID = HAL_DMAMUX1_REQ_GEN_TIM12_TRGO ; /* 请求触发器选择LPTIM2_OUT */
dmamux_ReqGenParams . Polarity = HAL_DMAMUX_REQ_GEN_RISING ; /* 上升沿触发 */
dmamux_ReqGenParams . RequestNumber = 1 ; /* 触发后,传输进行1次DMA传输 */
HAL_DMAEx_ConfigMuxRequestGenerator ( & DMA_Handle , & dmamux_ReqGenParams ); /* 配置DMAMUX */
HAL_DMAEx_EnableMuxRequestGenerator ( & DMA_Handle ); /* 使能DMAMUX请求发生器 */
/*##-4- 启动DMA双缓冲传输 ################################################*/
/*
1、此函数会开启DMA的TC,TE和DME中断
2、如果用户配置了回调函数DMA_Handle.XferHalfCpltCallback,那么函数HAL_DMA_Init会开启半传输完成中断。
3、如果用户使用了DMAMUX的同步模式,此函数会开启同步溢出中断。
4、如果用户使用了DMAMUX的请求发生器,此函数会开始请求发生器溢出中断。
*/
HAL_DMAEx_MultiBufferStart_IT ( & DMA_Handle , ( uint32_t ) IO_Toggle , ( uint32_t ) & GPIOB -> BSRRL ,( uint32_t ) IO_Toggle1 , 8 );
/* 用不到的中断可以直接关闭 */
//DMA1_Stream1->CR &= ~DMA_IT_DME;
//DMA1_Stream1->CR &= ~DMA_IT_TE;
//DMAMUX1_RequestGenerator0->RGCR &= ~DMAMUX_RGxCR_OIE;
TIM12_Config ( 0 );
}
// 功能说明: DMA1 Stream1中断服务程序
void DMA1_Stream1_IRQHandler ( void )
{
/* 传输完成中断 */
if (( DMA1 -> LISR & DMA_FLAG_TCIF1_5 ) != RESET )
{
/* 清除标志 */
DMA1 -> LIFCR = DMA_FLAG_TCIF1_5 ;
/* 当前使用的缓冲0 */
if (( DMA1_Stream1 -> CR & DMA_SxCR_CT ) == RESET )
{
/*
1、当前正在使用缓冲0,此时可以动态修改缓冲1的数据。
比如缓冲区0是IO_Toggle,缓冲区1是IO_Toggle1,那么此时就可以修改IO_Toggle1。
2、变量所在的SRAM区已经通过MPU配置为WT模式,更新变量IO_Toggle会立即写入。
3、不配置MPU的话,也可以通过Cahce的函数SCB_CleanDCache_by_Addr做Clean操作。
*/
}
/* 当前使用的缓冲1 */
else
{
/*
1、当前正在使用缓冲1,此时可以动态修改缓冲0的数据。
比如缓冲区0是IO_Toggle,缓冲区1是IO_Toggle1,那么此时就可以修改IO_Toggle。
2、变量所在的SRAM区已经通过MPU配置为WT模式,更新变量IO_Toggle会立即写入。
3、不配置MPU的话,也可以通过Cahce的函数SCB_CleanDCache_by_Addr做Clean操作。
*/
}
}
/* 半传输完成中断 */
if (( DMA1 -> LISR & DMA_FLAG_HTIF1_5 ) != RESET )
{
/* 清除标志 */
DMA1 -> LISR = DMA_FLAG_HTIF1_5 ;
}
/* 传输错误中断 */
if (( DMA1 -> LISR & DMA_FLAG_TEIF1_5 ) != RESET )
{
/* 清除标志 */
DMA1 -> LISR = DMA_FLAG_TEIF1_5 ;
}
/* 直接模式错误中断 */
if (( DMA1 -> LISR & DMA_FLAG_DMEIF1_5 ) != RESET )
{
/* 清除标志 */
DMA1 -> LISR = DMA_FLAG_DMEIF1_5 ;
}
}
// 功能说明: DMAMUX的中断服务程序,这里用于处理请求发生器的溢出。
void DMAMUX1_OVR_IRQHandler ( void )
{
if (( DMAMUX1_RequestGenStatus -> RGSR & DMAMUX_RGSR_OF0 ) != RESET )
{
/* 关闭溢出中断 */
DMAMUX1_RequestGenerator0 -> RGCR &= ~ DMAMUX_RGxCR_OIE ;
/* 清除标志 */
DMAMUX1_RequestGenStatus -> RGCFR = DMAMUX_RGSR_OF0 ;
}
}
ADC
STM32H7 支持三路 ADC,分别是 ADC1, ADC2 和 ADC3。其中 ADC1 和 ADC2 可以组成双 ADC模式, ADC3 是独立的。这个跟 STM32F4 有所不同, F4 的 ADC1, ADC2 和 ADC3 可以组成三 ADC模式。
可以配置为 16bit, 14bit, 12bit, 10bit 或者 8bit 分辨率, 分辨率越低可以做到的采样率越高,因为转换时间要短。
每个 ADC 都支持 20 路采样通道。 其中有 6 路快速通道和 14 路慢速通道,慢速和快速的区别主要是支持的最高采样率不同,慢速通道要比快速通道低。
支持单独输入和差分输入, 其中差分输入不支持负压测量
支持偏移校准和线性度校准, STM32F1 的时候还带校准功能,到了 STM32F4 取消掉了, H7 又恢复了校准功能。
支持规则通道和注入通道两种采样方式。
支持低功耗特性, 系统在低频工作时保持最佳 ADC 性能(提供自动延迟插入)。
具有五条专用的内部通道, 内部参考电压 VrefInt,内部温度传感器和 VBAT 监测通道 VBAT/4 都是连接到 ADC3。另外内部 DAC 通道 1 和通道 2,连接到 ADC2。
支持过采样,最高可以调整到 26bit 采样率。
ADC 采样的数据可接入 DFSDM 数字滤波器进行后期处理。
每个 ADC 支持三路模拟看门狗。
规则通道触发
注入通道触发
ADC 单端和差分的支持
单极性,双极性比较好理解, 就是单电源供电或者双电源供电, 这里的双电源是指的正负电压供电
单端输入模式下, 通道 i 转换的模拟电压是 VINP[i]正向复用引脚与 VREF-之差。
差分输入模式下, 通道 i 转换的模拟电压是 VINP[i]正向复用引脚与 VINN[i]反向复用引脚之差。
伪差分 AIP-AIN 就是第 5 幅图,内部 ADC 读取 AIP 和 AIN 的差值,但允许 AIN 上有一个很小的共模电压,比如正负 0.3V。
真差分是 AIP-AIN 就是第 2 幅或者第 5 幅图,其内部 AIP 和 AIN 分别有一个 ADC,分别读取转换AIP-GND,和 AIN-GND,再对这两个数字值做差,所以 AIN 上也可以接收很大的共模值。
ADC 过采样机制
ADC 校准问题
ADC 的 Vbat/4, VrefInt 和温度采样
Vbat/4 连接至 ADC3_INP17,所以可以使用 ADC3 的通道 17 进行测量。
VrefInt 连接至 ADC3_INP19,所以可以使用 ADC3 的通道 19 进行测量。 可以通过监测内部电源模块参考电压 VrefInt 来评估 ADC Vref+电压的参考值。 VrefInt 的测量框图如下.
STM32H7 带有温度传感器,可以使用 ADC3_INP18 进行测量,不过读取出来的还是个电压值,需要将其转换为温度值.
定时器触发配合 DMA 双缓冲
ADC->DMA Stream1->RAM
DMA 方式多通道采样
FMC 总线
SDRAM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// ll库
void SDRAM_Initialization_Sequence ( SDRAM_HandleTypeDef * hsdram )
{
__IO uint32_t tmpmrd = 0 ;
/* Configure a clock configuration enable command */
Command -> CommandMode = FMC_SDRAM_CMD_CLK_ENABLE ; // 开启SDRAM时钟
Command -> CommandTarget = FMC_COMMAND_TARGET_BANK ; // 选择要控制的区域
Command -> AutoRefreshNumber = 1 ;
Command -> ModeRegisterDefinition = 0 ;
HAL_SDRAM_SendCommand ( hsdram , Command , SDRAM_TIMEOUT ); // 发送控制指令
HAL_Delay ( 1 ); // 延时等待
/* Configure a PALL (precharge all) command */
Command -> CommandMode = FMC_SDRAM_CMD_PALL ; // 预充电命令
Command -> CommandTarget = FMC_COMMAND_TARGET_BANK ; // 选择要控制的区域
Command -> AutoRefreshNumber = 1 ;
Command -> ModeRegisterDefinition = 0 ;
HAL_SDRAM_SendCommand ( hsdram , Command , SDRAM_TIMEOUT ); // 发送控制指令
/* Configure a Auto-Refresh command */
Command -> CommandMode = FMC_SDRAM_CMD_AUTOREFRESH_MODE ; // 使用自动刷新
Command -> CommandTarget = FMC_COMMAND_TARGET_BANK ; // 选择要控制的区域
Command -> AutoRefreshNumber = 8 ; // 自动刷新次数
Command -> ModeRegisterDefinition = 0 ;
HAL_SDRAM_SendCommand ( hsdram , Command , SDRAM_TIMEOUT ); // 发送控制指令
/* Program the external memory mode register */
tmpmrd = ( uint32_t ) SDRAM_MODEREG_BURST_LENGTH_1 |
SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL |
SDRAM_MODEREG_CAS_LATENCY_3 |
SDRAM_MODEREG_OPERATING_MODE_STANDARD |
SDRAM_MODEREG_WRITEBURST_MODE_SINGLE ;
Command -> CommandMode = FMC_SDRAM_CMD_LOAD_MODE ; // 加载模式寄存器命令
Command -> CommandTarget = FMC_COMMAND_TARGET_BANK ; // 选择要控制的区域
Command -> AutoRefreshNumber = 1 ;
Command -> ModeRegisterDefinition = tmpmrd ;
HAL_SDRAM_SendCommand ( hsdram , Command , SDRAM_TIMEOUT ); // 发送控制指令
HAL_SDRAM_ProgramRefreshRate ( hsdram , 1543 ); // 配置刷新率
}
SD卡
驱动
FATOS
LittleOS
LTDC 屏幕驱动
LCD 的 DE 同步模式和 HV 同步模式的区别
DE
DE 为高电平时,表示当前 CLK 周期内传输的是有效像素;
DE 为低电平时,数据无效(如消隐区)。
HV
VSYNC:标识一帧开始;
HSYNC:标识一行开始;
LTDC 背景层,图层 1, 图层 2 和 Alpha 混合
背景层:不是真正的“图层”,而是当 Layer 1/2 未覆盖区域显示的(LTDC->BCCR) 无像素缓冲区,不参与 Alpha 混合。
图层 1:可配置的图像层,有自己的显存地址、颜色格式、位置、Alpha 值等。
图层 2:优先级最高
Alpha 混合模式
Constant Alpha(常量 Alpha):整个图层统一透明度(0~255,0=全透明,255=不透明)
Pixel Alpha(像素 Alpha):每个像素自带 Alpha 通道(如 ARGB8888 格式)
LTDC 的水平消隐和垂直消隐
水平消隐就是 LCD 扫描一行结束到另一行开始的时间,这段消失的时间就是水平消隐,即 HSYNC宽度+ HBP + HFP 这段消失的时间。
垂直消隐就是 LCD 扫描最后一行结束到第一行开始的时间,这段消失的时间就是垂直消隐,即 VSYNC宽度+ VBP + VFP 这段消失的时间。
区分 FPS 帧率和刷新率
FPS:电脑渲染的帧率
刷新率:屏幕刷新率,与时钟频率相关
避免 LTDC 刷新撕裂感的解决办法
LTDC 刷新还在垂直消隐期间就将整个界面刷新完成,而我们如何只知道 LTDC 在垂直消隐期,通过函数 HAL_LTDC_ProgramLineEvent 设置刷新到指定行时进入中断即可,一般设置为第 0 行进入中断,然后设置个标志即可。一旦检测到这个标志,就通过 DMA2D 快速将界面刷新好,这样就有效的避免了撕裂感
Usage
LTDC 寄存器结构体 LTDC_TypeDef
LTDC 参数初始化结构体 LTDC_InitTypeDef
LTDC 图层配置结构体 LTDC_LayerCfgTypeDef
LTDC 句柄结构体 LTDC_HandleTypeDef
基本函数
HAL_LTDC_Init
HAL_LTDC_ConfigLayer
HAL_LTDC_SetAlpha
HAL_LTDC_Reload
HAL_LTDC_SetPixelFormat
HAL_LTDC_SetWindowPosition
HAL_LTDC_SetWindowSize_NoReload
电阻触摸和电容触摸
电阻触摸芯片 STMPE811 其实就是 ADC,返回的是 ADC 数值,而电容触摸芯片 GT811, GT911 和FT5X06 返回的是显示屏实际的坐标值。
使用电阻触摸芯片 STMPE811 需要做触摸校准,而使用电容触摸芯片 GT811, GT911 和 FT5X06 是自动校准的,无需手动校准。
过程
LTDC 显存使用 SDRAM
LTDC 的颜色格式是 32 位色 ARGB8888, 那么所需要显存大小(单位字节)是: 显示屏宽 * 显示屏高 * (32/8) , 其中 32/8 是表示这种颜色格式的一个像素点需要 4 个字节来表示。 又比如配置颜色格式是 16 位色的 RGB565, 那么需要的显存大小是: 显示屏宽 * 显示屏高 *(16/8) ,其中 16/8 是表示这种颜色格式的一个像素点需要 2 个字节来表示。其它的颜色格式, 依此类推。
LTDC 涉及到的引脚配置
LTDC 时钟和时序配置
具体查询屏幕手册
如何验证 LTDC 的时序配置是否正确
1
2
3
4
5
LCD_SetBackLight ( BRIGHT_DEFAULT );
// 设置为红色
hltdc_F . Init . Backcolor . Blue = 0 ;
hltdc_F . Init . Backcolor . Green = 0 ;
hltdc_F . Init . Backcolor . Red = 255 ;
LTDC 图层配置
LCD 背光实现
LCD 的背光是 PWM 驱动方式
点阵字体和字符编码
GB2312
第一个字节从编号 0 到编号 127 的字符不变,还是表示 ASCII, 而之后的 0xA1 到 0xFE 用于汉字编码,这个字节被称为汉字的区号或者高位字节, 0xA1 到 0xFE 换算成区号就是从 01 区到 94 区
第二个字节的 0xA1 到 0xFE 用于汉字编码, 这个字节被称为汉字的位号或者低位字节, 0xA1 到 0xFE换算成位号就是从位号 01 到位号 94(换算关系就是对编码值减去 0xA0)。 根据区号和位号的设置, 那么就有 94*94 = 8836 个编码可供使用。 在这些编码里,我们还把数学符号、罗马希腊字母、日文的假名都编码进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码, 即全角字符,而原来在 127 号之前的那些字符称为半角字符。
电阻屏触摸校准原理(两点)
比如我们操作的显示屏分辨率是 800x480, 电阻触摸芯片采用 STMPE811(12 位ADC, 触摸值范围 0-4095), 获得当前的触摸值是(1024, 2048) ,按照比例关系转换成坐标值就是(1024x800/4096, 2048x800/4096), 即(200, 400) 。采用这种方法效果不好,容易出现点击不准确的问题。
鉴于此原因, 需要通过触摸校准在 ADC 数值和显示屏分辨率之间建立一个新的线性关系,**简单的说就是由比例关系 y = ax 升级为 y = ax + b。**如果有了新的触摸 ADC 数值,代入这个线性关系里面就可以得到当前实际的坐标值,触摸校准的作用就在这里了。具体实现原理图如下:在左上角和右下角分别设置两个坐标点(LcdX0, LcdY0)和(LcdX1, LcdY1) , 然后让用户去点击,会得到两组 ADC 数值(AdcX0, AdcY0)和(AdcX1, AdcY1) 。
DMA2D
DMA2D 主要实现了两个功能,一个是 DMA 数据传输功能,另一个是 2D 图形加速功能。
DMA 数据传输:主要是两种方式, 一个是寄存器到存储器,另一个是存储器到存储器。 通过 DMA 可以大大降低 CPU的利用率。
2D 图形加速功能:支持硬件的颜色格式转换和 Alpha 混合效果。
DMA2D 工作模式
DMA2D 支持的工作模式如下:
模式 1:寄存器到存储器模式这个模式主要用于清屏, 也即是将显示屏清为单色效果。
模式 2:存储器到存储器模式:这个模式用于从一个存储器复制一块数据到另一个存储器,比如将摄像头 OV7670 的输出图像复制到LCD 显存就可以采用这种方式。
模式 3:存储器到存储器模式, 带颜色格式转换:这个模式比模式 2 多了一个颜色格式转换,比如我们要显示一幅 RGB888 颜色格式的位图到 RGB565颜色格式的显示屏, 就需要用到这个模式, 只需输入端配置为 RGB888, 输出端配置 RGB565 即可。位图颜色格式转换后会显示到显示屏上。
模式 4:存储器到存储器模式, 带颜色格式转换和混合:这个模式比模式 3 多了一个混合操作, 通过混合,可以将两种效果进行混合显示。
模式 5:存储器到存储器模式, 带颜色格式转换和混合,前景色是固定的:同模式 4, 只是前景色的颜色值是固定的。
Usage
DMA2D 寄存器结构体 DMA2D_TypeDef
DMA2D 参数初始化结构体 DMA2D_InitTypeDef
DMA2D 的图层结构体 DMA2D_LayerCfgTypeDef
DMA2D 句柄结构体 DMA2D_HandleTypeDef
常用操作
_DMA2D_Fill
_DMA2D_Copy
_DMA2D_MixColorsBulk
_DMA2D_AlphaBlendingBulk
_DMA2D_DrawAlphaBitmap
转换 PNG 图片为 ARGB8888 格式位图
下载小软件 BmpCvt
硬件 JPEG 编解码
Usage
JPEG 寄存器结构体 JPEG_TypeDef
JPEG 的编解码参数结构体 JPEG_ConfTypeDef
JPEG 结构体句柄 JPEG_HandleTypeDef
查询式编解码函数
HAL_JPEG_Encode
HAL_JPEG_Decode
中断方式
HAL_JPEG_Encode_IT
HAL_JPEG_Decode_IT
DMA 方式
HAL_JPEG_Encode_DMA
HAL_JPEG_Decode
如果用户之前的数据已经处理完毕,需要插入新数据, 会调用函数HAL_JPEG_GetDataCallback
输出缓冲区填充了给定大小的数据后,会调用回调函数 HAL_JPEG_DataReadyCallback
JPEG 解码时,如果解码成功,会调用回调函数 HAL_JPEG_InfoReadyCallback。
JPEG 编码操作结束后会调用回调函数 HAL_JPEG_EncodeCpltCallback。
JPEG 解码操作结束后,会调用回调函数 HAL_JPEG_DecodeCpltCallback。
操作过程中出现错误,会调用回调函数 HAL_JPEG_ErrorCallback,用户可以调用函数HAL_JPEG_GetError 获取错误类型。
HAL JPEG 默认使用的是 ISO/IEC 10918-1 规格量化表,如果要修改,可以调用函数HAL_JPEG_SetUserQuantTables 实现。
通过函数 HAL_JPEG_GetState 可以获取 JPEG 状态。
LVGL
SDMMC
“格式化软件下载”
DAC
RTT
RTT + DHT11 + ESP8266(mqtt)