你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【学习指南】基于STM32G474VET6 开发板基础实验经验分享一

[复制链接]
攻城狮Melo 发布时间:2024-11-22 14:02
一、RCC 实验  G+ o' ]- ^+ G3 O5 T% t
实验目的:掌握和熟悉时钟系统的配置,了解时钟树,时钟源,以及时钟输出。
# @( T' l( x) k2 x$ n; ^0 P! m: l# J% ~/ V: Y9 v9 F0 m

0 J9 E; \& @! r; n- ]: m. j4 e1、LSCO(使用 LSE)低频时钟输出实验" c. x& }; F* d. h$ z
CubeMX 配置如下,保存后生成对应的配置代码:
2 {2 ]/ S. ]8 k& n  b
微信图片_20241122131728.png 6 T: y8 B! ~' }8 a' F
▲ CubeMX 配置时钟输入
微信图片_20241122131725.png 8 o) w5 R! |- Q5 N8 t
▲ CubeMX 配置 LSCO 的时钟源
通过配置时钟树,使用外部低速时钟作为时钟输入,并且在引脚上输出时钟信号。本实验主要展示外部低频时钟的使用。" a) p9 L3 g. U5 |$ T: w" W# U
6 T# N( c3 m  H) e! k

% a8 q' s. l* u, `3 p* n. mLSCO 的输出主要函数说明:
, p5 @! E6 ^) |void HAL_RCCEx_EnableLSCO(uint32_t LSCOSource)
6 x4 y0 v. m5 Q7 |& {! B9 a3 m" Y4 ]% n' h, d4 Y  m+ {
: z; o; _# L! w& u/ N
功能:开启 LSCO 时钟输出;
2 j! J8 g+ r% O( @2 W4 `参数 1:LSCO 时钟源,可选 RCC_LSCOSOURCE_LSI 和 RCC_LSCOSOURCE_LSE;5 n- Z% K/ v- r  B
返回:无;使用举例:HAL_RCCEx_EnableLSCO ( RCC_LSCOSOURCE_LSE);//开启 LSCO 输出,时钟源为外部低速时钟 LSE;
1 _9 R8 Q/ ^: B( f+ Q注 意 :此 函 数 在 CubeMX 配 置 时 钟 输 出 之 后 自 动 生 成 并 被 调 用 , 调 用 位 置 为SystemClock_Config 函数的最后。6 H2 n0 b' D2 w+ ~

) C6 w3 s9 s+ h  Z* S( |6 M

+ V; I7 E. `3 g. O实验现象:
( n/ ^6 A% f8 h通过 CubeMX 配置之后,将代码直接编译烧录即可在 PA2 上观察到一个频率大约为32.768kHz 的时钟信号。
& H  \) ]8 w$ @; T4 @9 Z
. {# R- T4 j1 {6 ]' z
) g8 @) n( _  ]
2、MCO(使用 HSE)系统时钟输出实验4 t# z/ S" h  p, R) ]
CubeMX 配置如下,保存后生成对应的配置代码:6 \, i2 s5 S# y% ]
微信图片_20241122131723.png 1 O' [) L5 [8 ^; q, J( L
▲ CubeMX 配置 HSE 输入以及 MCO 输出使能
微信图片_20241122131719.png ' U  ^& r+ Z/ ?& A" u# q7 X/ c8 @
▲ CubeMX 配置 MCO 输出速度
微信图片_20241122131716.png
- N* j( X- @2 p# d; Y9 H& G# P% T
▲ CubeMX 配置锁相环与 MCO 源
通过配置时钟树,锁相环,实现倍频系统时钟,并通过 MCO 进行时钟信号输出。本实验主要展示外部时钟输入 HSE 的使用以及通过锁相环配置系统时钟。
MCO 的输出主要函数说明:
void HAL_RCC_MCOConfig ( uint32_t RCC_MCOx, uint32_t RCC_MCOSource, uint32_tRCC_MCODiv )
0 {% U. @& ~/ M6 f
功能:开启 MCO 时钟输出;
参数 1:MCO 时钟源输出引脚,可选内容为:RCC_MCO_PA8、RCC_MCO_PG10;需要注意的是,对于 G4 系列该参数只能为 RCC_MCO_PA8;
参数 2:MCO 时钟源,可选内容为:RCC_MCO1SOURCE_NOCLOCK、RCC_MCO1SOURCE_SYSCLK、 RCC_MCO1SOURCE_HSI、RCC_MCO1SOURCE_HSE、RCC_MCO1SOURCE_PLLCLK、RCC_MCO1SOURCE_LSI、RCC_MCO1SOURCE_LSE、RCC_MCO1SOURCE_HSI48;
参数 3:MCO 时钟输出分频,可选内容为:RCC_MCODIV_1、RCC_MCODIV_2、RCC_MCODIV_4、RCC_MCODIV_8、RCC_MCODIV_16;
返回:无;
使用举例:HAL_RCC_MCOConfig ( RCC_MCO1, RCC_MCO1SOURCE_SYSCLK,RCC_MCODIV_1 ) ;//开启 MCO 输出,时钟源为外部低速时钟 SYSCLK,时钟分频为 1 分频(不分频);
注 意 :此 函 数 在 CubeMX 配 置 时 钟 输 出 之 后 自 动 生 成 并 被 调 用 , 调 用 位 置 为SystemClock_Config 函数的最后。; V9 L* o4 `5 c% Y$ O' f
! {9 Q1 a8 S2 h
2 D( e5 d  X% {! X  f0 ]' U9 ^: v
实验现象:% _$ t0 q" U. E. i$ e, T: C% r# d
通过 CubeMX 配置之后,将代码直接编译烧录即可在 PA8 上观察到一个频率大约为 40MHz的时钟信号。3 e# O5 Y  i2 D* k: H) ~
8 y0 i2 T: x# l) }

! b* V. L2 G1 T: T9 M9 M) k7 b二、GPIO 实验
: a  p1 n9 l) V9 x  P( V. _! {实验目的:掌握和熟悉 GPIO 的基本输入与输出功能。" p( D+ d. R3 i; x% k
$ H1 g2 `; K4 k, @: G
- s# W: I$ B2 D  i4 a; \
1、GPIO 输出实验
9 W$ D0 v/ C; v) zCubeMX 配置如下,保存后生成对应的配置代码:5 X3 h: P  t: R/ m; H! I) G
11.png
: b! B) r# e5 Z- N
▲ 图 3.2.1 基本系统配置
微信图片_20241122131710.png + H3 p( `( b. I' M/ |
▲ 图 3.2.2 时钟树配置
微信图片_20241122131708.png
, E: S+ @8 u: p7 |  H; j
▲ 3.2.3 IO 配置
配置完成后,可以使用系统延时函数延时控制 LED 闪烁,来验证功能。注意上述系统时钟配置仅在此进行一次展示,下文中如果使用 170MHz 主频则不再进行时钟配置展示。/ K: s# w# i6 S$ j& [; h8 {

4 E1 k8 G( p( L# t. i

" \2 }# z3 ]: ?1 e7 @: e$ P' }系统延时函数说明:
* u+ j7 T/ P8 ]1 [7 k+ V. ^) I
0 J4 u$ B; G. E6 ~/ d& O
  1. __weak void HAL_Delay(uint32_t Delay)
复制代码

$ q* V# t1 J0 k功能:进行阻塞式系统延时;9 F( _0 o' ]& u% l
参数 1:延时时间,默认单位 ms,该阻塞时间是基于 HAL 时基产生的,默认情况下为滴答定时器,频率为 1kHz,如果该频率改变,那么延时时间单位也会随之改变;
! b' g8 ]5 r# y7 u+ f, p+ O! v返回:无;, L+ a2 g& Y1 D! y
使用举例:HAL_Delay(500);//延时 500ms
2 ]7 `  T( ?" H% p+ W& N注意:此函数使用滴答定时器中断来获得时基,从而使用 while 占用 cpu 进行延迟,在该main 中使用该函数进行延时能够被中断所打断。此函数一般不在中断中使用,如果在中断优先级高于滴答定时器的中断中使用会导致滴答定时器中断无法正常运行,从而使该函数彻底卡住无法退出。2 P! O: S: a  Z: R* V- H
- j. m; b; P" I, s- {
+ P% {8 }, Y, u0 `% n$ _, l- }
GPIO 输出操作函数说明:" i1 V# s. [. ~: M8 Q! s
void HAL_GPIO_WritePin ( GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState )
# i4 U2 q: Q/ I- [& }4 ]1 P$ Q8 z: m6 {
- t! u# U; b, S: ~
功能:写入 GPIO 输出电平;6 w/ [5 }2 _# N9 I
参数 1:GPIO 分组,可选 GPIOA,GPIOB,GPIOC 等,根据使用芯片类型进行选择;& O5 Y+ b8 O4 I; O
参数 2:GPIO 引脚号,一般定义为 GPIO_PIN_x,其中 x 可选 0-15,根据使用芯片类型进行选择;
" Q2 Z  o3 Q7 i# J参数 3:输出电平,可选 GPIO_PIN_SET(输出高电平)和 GPIO_PIN_RESET(输出低电平);
; K! w# y+ W4 [返回:无;
' v6 c6 z& k" |! n& V; |0 Y注意:CubeMX 支持自定义引脚标签,对标签进行过自定义之后会在 main.h 文件中生成响应的宏定义。比如上述例子中,定义 PD10 为 LED1,那么在 main.h 中则生成了#define LED1_Pin GPIO_PIN_10 和#define LED1_GPIO_Port GPIOD 两个宏定义,此时可以使用LED1_GPIO_Port 和 LED1_PIN 来代替函数中的参数 1 和参数 2.
: z- b( I0 J1 m3 Q使用举例:HAL_GPIO_WritePin ( LED1_GPIO_Port,LED1_Pin,GPIO_PIN_RESET );//输出低电平,点亮 LED
5 S2 R- r! f7 M: y
  1. void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
复制代码
# w1 j7 o% L" y5 `
功能:翻转 GPIO 输出电平;
$ l4 h% x, V' X( G* V1 X& m参数 1:GPIO 分组,可选 GPIOA,GPIOB,GPIOC 等,根据使用芯片类型进行选择;
. ^; l% ]" V2 O' w参数 2:GPIO 引脚号,一般定义为 GPIO_PIN_x,其中 x 可选 0-15,根据使用芯片类型进行选择;
% ~0 ^* x8 j  W4 S! y返回:无;
  ?% N1 W- ~& Z3 c1 ^使用举例:HAL_GPIO_TogglePin ( LED1_GPIO_Port,LED1_Pin );//翻转 LED1 的输出电平。5 E- G8 j% t  t  ^$ O; ~2 s
; q% u) L7 O0 _, ~/ i  o

$ R5 ?* t2 A$ d7 b, n/ @核心代码:  w9 S5 S4 s# W
  1. while (1)& K1 d4 h2 L8 i! D  I8 S6 a
  2. {
    0 F& c6 a. [# S' H( N" Z
  3. HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);//翻转 LED1 输出电平; C8 N* @' f2 h
  4. HAL_Delay(500);//延时 500ms- \8 h5 F  \* x8 B
  5. }
复制代码
3 o9 x' I/ I% i2 [
段代码为 main 函数中最后的 while(1)循环部分,进行 LED1 的输出电平翻转后延时500ms,不断循环。2 d+ u2 j. k) y, I5 V6 J6 l. M$ e

1 L& V7 S0 ^4 k$ C, C# N0 d

9 v: M6 ^0 f; D7 Y* n实验现象:
+ f. c; s: B, M+ V: G下载烧录后可以观察到 LED1 以 1Hz 频率进行闪烁,即每 0.5s 进行一次亮灭变化。
, u( L! A  }1 m8 N" }" |+ j) e% ]$ q4 q
2、GPIO 输入实验4 b2 E8 X7 J; r" U2 [( {
CubeMX 配置如下,保存后生成对应的配置代码:
! M3 q/ M7 G6 R+ s: ^; k8 X
微信图片_20241122131706.png
( L4 t( Q  n  \  R; s! b6 f5 D- ~
图 3.2.4 输入模式 IO 配置
除 PA4 配置为输入模式,另外 PD10 配置为输出模式用于驱动 LED1,PD10 的配置与GPIO 输出示例相同,此处不再演示,时钟树配置同样与上例相同。
' p/ t6 n# z. V2 n) z( M  ]+ e0 T+ `" t5 y4 f6 Y4 A
GPIO 输入操作说明:( w2 I. o8 a* ^, o; L
  1. GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
复制代码
1 a, a; h& u& m! u# \
功能:读取 GPIO 输入电平;6 B: {0 z2 s8 r, Z7 s2 r) M5 F
参数 1:GPIO 分组,可选 GPIOA,GPIOB,GPIOC 等,根据使用芯片类型进行选择;# O7 R) \+ o1 g8 p
参数 2:GPIO 引脚号,一般定义为 GPIO_PIN_x,其中 x 可选 0-15,根据使用芯片类型进行选择;
% b5 \# t* h) b/ O+ W' k! _, a4 f返回:GPIO 电平状态,可以为 GPIO_PIN_SET(高电平)和 GPIO_PIN_RESET(低电平);
4 q) B1 A2 Q( l* r( c使用举例:value=HAL_GPIO_ReadPin ( GPIOA,GPIO_PIN_4);//检测 PA4 输入电平
" i5 P3 u: j; r
% g- v+ q, L; }! ~

' c3 G# H0 i7 p! G6 L8 d9 _7 Z核心代码:
- y! X8 G6 o2 _6 @8 H7 m" gwhile (1)* U0 p4 ?& I4 v, b- g8 F1 r
{
4 |+ s. z/ X1 R1 w( J5 U/ o' k( N; uif(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_4)==0)//检测按下 PA4, ?  B$ [1 \) X) R0 f
{
3 P) J  n# G) b- g4 [HAL_Delay(5);//延时消抖6 w7 f, R. A$ M* ^( A. \: w
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_4)==0)//再次检测按下 PA4
8 `* @1 }: ~) t{
) z7 f3 h4 E; D' v* THAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);//翻转 LED1 输出电平
" P9 F* n9 ]. @while(!HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_4));//等待松开 PA47 v9 k( {* l) U. G6 [, \- J; X
}/ v: U3 h1 H; ^$ ]: P& z
}
5 r# J' `  G/ B: H4 _. j8 ?}
. V% T1 H0 w, Z7 c2 x- l! M' s7 E* S* \3 S; c
2 b% H* C- d& g- w- T  N. L5 t
本段代码为 main 函数中最后的 while(1)循环部分,不断进行 PA4 电平检测,检测到低电平,即按下,则进行 5ms 延时消抖之后再进行检测,以确认按下,则进行 LED1 的输出电平,然后等待松开按键。
/ C- m0 B' ~" z( D. _0 R8 n: V+ n* w" ?( X
: M0 G; c1 K9 S) W3 _2 V
实验现象:+ z+ N# ^; \4 `: y  o# I' q
下载烧录后可以观察到按一下 KEY1,LED1 的亮灭状态进行一次翻转。
( H& J% _+ I1 [3 r: G4 h. p2 A4 v
5 ~4 Q+ ~# W8 f  t
三、中断系统实验! x0 Q  F% S) Y0 t: E
5 q, f+ P6 _  @% N
实验目的:掌握和熟悉外部中断和中断嵌套的使用方法。
; s5 F- E/ ]' b& T
7 ~+ T9 C/ ]- `/ U1、外部输入中断实验# C4 H8 T- z9 j6 i/ f5 s% o6 G% x5 e
CubeMX 配置如下,保存后生成对应的配置代码:+ ?! z8 G$ F0 H' J) `, Z
微信图片_20241122131703.png
. ^. p2 I$ ~7 A0 p0 ?
▲ GPIO 外部输入中断配置

( @' k5 k- t4 @; A1 A$ G
微信图片_20241122131701.png
' B0 @/ _' }7 c6 P, a
▲ GPIO 外部输入中断优先级配置
除 PA4 配置为下降沿触发外部中断模式,另外 PD10 配置为输出模式用于驱动 LED1,PD10的配置与 GPIO 输出示例相同。
" O9 H1 v+ {9 m: W" ^
: l- _4 P  h7 F7 W& A, L  R4 B
GPIO 外部输入中断函数说明:: X- Z/ s$ N  P$ o: M
void EXTI4_IRQHandler(void)5 B9 \7 q6 L; v  m; y) c; \
{
: \! h$ a8 ~! n" N9 W0 yHAL_GPIO_EXTI_IRQHandler(GPIO_PIN_4);
* A: R5 n, E. J4 Z  Y: M5 ]7 Y}" j  K2 {! Y2 T* ^1 g; d0 o' M
功能:EXTI4 中断处理函数;0 h$ [4 P& y5 [1 e+ J1 J& `6 b) ]
参数:无;5 s2 O9 d( t" d" g/ y- t7 ~3 s3 P
返回:无;
  K. C7 H/ L) s2 i( Q7 B% k4 X# V. }5 |说明:当满足 EXTI4 的中断触发条件,CPU 自动跳转到此函数进行执行,CubeMX 生成该函数在 stm32g4xx_it.c 文件中,该函数内为具体的中断处理,最后会跳转到中断回调函数中。
: I' N( N. Y0 u0 M% D6 Y
  1. __weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
复制代码

. J2 H. `' E% P& v0 M功能:EXTI 中断回调处理函数;! `2 \& P2 U. j7 i
参数:GPIO 号;4 X$ T1 D# ]4 o4 }# r- R
返回:无;
$ Z& z. T+ ~+ b说明:EXTI 中断回调函数,当触发 EXTI 中断之后,中断服务函数会自动调用此回调函数,并填写触发此次中断的 GPIO 号,需要注意的是,此回调函数为多个 EXTI 所共用的,所以进入之后需要对触发中断的 IO 进行判断,另外此函数在 CubeMX 中生成的代码为弱定义代码,可以在需要的地方进行重定义。
* t2 }# @/ V- v. I# O% S/ t6 ?$ k& h9 t! L, T  O% A; J$ ]4 J
" F3 w' V  ]$ l
核心代码:& D1 i' d4 {( T, v7 [3 C
//外部中断处理* f/ B3 B) O7 q( I
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)6 P2 G. ]& W% a( ?
{
2 m) w% R1 Q- i9 f) o& \! S' Q3 Cif(GPIO_Pin == GPIO_PIN_4)//PA4 中断
7 [* j$ c/ a: K% T: Y5 d* }- x{
$ w2 r; A2 {9 {3 l$ ?0 d. l: Gfor(uint16_t i=0;i<65500;i++)//等待一段时间' }  F+ \: i- m! F! H' x  \
{
7 o3 @* t# S$ i4 C  i1 |__NOP();
5 _( n/ ]! w; T+ G$ N$ R1 f; S}
7 W( {0 M6 L; |5 Uif(!HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_4)) //读取为低电平,确认按下3 D5 c2 F" ^/ z
{
' a  h/ v( I4 N9 W1 a& PHAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);//翻转 LED1 电平
& j9 n( Q; c; t! r+ @6 O) A}! r' W. p6 }8 M6 A
}
0 t/ S. L$ l& w' G" T}% T4 [; E: L$ s
5 c, |0 K  \0 {! K
本段代码为 EXTI 的中断处理回调函数,进入此函数之后首先判断触发中断的 IO 为所设置的 GPIO,然后等待消抖,再次进行电平检测以防误触,最后进行 LED1 翻转。此函数可以放在任意位置,本例中放在了 main 函数之后。' A, W7 l* ^! k2 M) K6 P
4 D3 v# q" o  H/ o) q
) C  h9 U+ A" k0 G( n$ e5 W
实验现象:
: t5 \" X* d0 _( U下载烧录后可以观察到按一下 KEY1,LED1 的亮灭进行一次翻转。7 T( B  A( }! @2 r

( }: A, _' U1 o$ N2 s/ G3 E

+ F4 l$ w1 j" T' D3 ]' g5 T2、中断嵌套实验! V$ F9 c% l9 V+ c# I
CubeMX 配置如下,保存后生成对应的配置代码:- _( z9 r" C0 A' y% G$ F' O
微信图片_20241122131657.png
4 B5 I' n  n1 W% z9 g8 c
▲ 外部中断以及输出 IO 配置
微信图片_20241122131655.png ) ]+ j6 V5 k$ W
▲ 中断优先级配置
PA4,PA5 配置为下降沿触发外部中断模式,另外 PD10,PD11 配置为输出模式用于驱动LED1 和 LED2,PD10 和 PD11 的配置与 GPIO 输出示例相同。本例主要演示中断嵌套过程,涉及嵌套的中断分别为滴答定时器中断,EXTI4 中断,EXTI[9:5]中断,这三者优先级依次降低。$ H* x; e( N6 l7 Y1 ?
2 N0 S+ A0 g" X/ t  p* j" U. n, `: W
核心代码:
! q+ x; V; C  W7 G# K: Q! I9 l
//外部中断处理% ?+ \# B2 W4 o6 q0 T( A# I
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
$ t( T( i; `5 c( c' I# ?* Y4 c{0 ]( e: T8 ?( w- c( Z
if(GPIO_Pin == GPIO_PIN_4)//PA4 中断
. Z) O, ]2 E" q: I" B! f! `{% F  ]7 \' l  T  @% K5 X
HAL_Delay(5);//延时消抖
9 e: L/ a; ?8 C# \- }; l$ Mif(!HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_4)) //读取为低电平,确认按下
9 {* p5 p4 l( W3 S; n5 L1 B{
* @. K/ d) i. U# Z3 b# ^HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);//翻转 LED
% [- @* V; N7 W6 h4 c- dwhile(!HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_4));//等待松手,卡住函数" h  i% i) R/ k' H: N6 g
}
5 P7 }+ Z$ [& S$ p0 Y4 @: A# ~}+ T: `5 e! N, V
else if(GPIO_Pin == GPIO_PIN_5)//PA5 中断
+ p  h: i, X0 O{
3 X. ?  c% V5 tif(!HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_5)) //读取为低电平,确认按下
6 J1 @' \" J) s4 I+ Z& c{) r, e, s( [3 M" }" G
HAL_Delay(5);//延时消抖, N) q% c' U( g
HAL_GPIO_TogglePin(LED2_GPIO_Port,LED2_Pin);//翻转 LED24 A: s# u+ @8 L* ?/ ~
while(!HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_5));//等待松手,卡住函数执行
7 V" w5 X4 W/ V5 y" z2 @, j$ o}
  \4 u, j: v9 t% H8 X}, U; e! p% }1 }. E  e, t
}, S/ @; Y, L% N( S  P
. l$ T, a9 L+ }# v3 V9 F
本段代码为 EXTI 的中断处理回调函数,进入此函数之后首先判断触发中断的 IO 为所设置的 GPIO,然后等待消抖,再次进行电平检测以防误触,最后进行 LED 翻转。这里等待采用了 HAL_Delay 函数,该函数需要滴答定时器支持,因为滴答定时器配置优先级高于外部中断,因此能够打断外部中断的处理函数正常工作,而 EXTI4 中断优先级高于 EXTI[9:5],因此前者能够打断后者进行嵌套。
7 R& Q. `' ?$ ~) \( r& H! H0 n1 p+ N0 P0 x" H3 Z% q7 H

, X: [( R! b" K# J1 e实验现象:$ s5 B/ t. b% a. }$ ~2 `: g# ]8 B
下载烧录后可以观察到按一下 KEY1,LED1 的亮灭进行一次翻转,按一下 KEY2,LED2亮灭进行一次翻转,当按住 KEY1 来按 KEY2 时,LED2 无法正常翻转,按住 KEY2 来按KEY1 时,LED1 可以正常翻转。
6 H; _# {  _/ O
0 }8 [' h- p& N: t' x$ u0 L
! g4 p' V5 I- o# C
四、定时器实验
- b, ?1 F7 v4 z; Z1 q6 C实验目的:掌握和熟悉定时器中断、PWM 输出以及输入捕获的使用和配置方法,以及掌握DMA 方式传输启动和级联定时器等高级操作。
2 Q% t0 _! @! u2 j- ]- J
9 ^! C9 K! R% e6 q$ l1、定时中断实验  y0 a' R0 a! i; N+ n0 m& J
CubeMX 配置如下,保存后生成对应的配置代码:6 i* H1 U$ A' v: w
微信图片_20241122131652.png
7 @" j- C( y& r
▲ 定时器中断配置
微信图片_20241122131650.png 1 o8 X5 q1 T; @  U1 c
▲ 使能定时器中断
本实验使用 TIM1 作为中断定时器,使用 TIM1 的更新中断,配置系统时钟为 170MHz,同时将 PD12 配置为输出 IO,用于驱动开发板上的 RGB 灯闪烁。
5 |% Y' p, Y5 @1 {6 c4 I6 X8 g1 ^7 R1 x8 C1 ~. e
定时器基本操作函数说明:
1 U$ Q3 K/ S0 h# ~( e$ c
  1. HAL_StatusTypeDef HAL_TIM_Base_Start(TIM_HandleTypeDef *htim)
复制代码
& s* }8 x8 t1 x* @, h
功能:以普通方式启动定时器;* m3 J6 n( V  x+ D' z; V
参数 1:定时器句柄,根据实际需要填写;
5 s  k8 j" ]: j; T$ J: H5 a% u返回:操作结果,返回 HAL_OK 或者 HAL_ERROR;8 C& \/ b- @( U2 O7 c: ?
注意:用这种方式启动定时器不会触发定时器中断,中断不会被使能
. c; _6 _2 y: y  ?: Q
  1. HAL_StatusTypeDef HAL_TIM_Base_Stop(TIM_HandleTypeDef *htim)
复制代码

& B& k, I# j0 a( d) Y: u0 }功能:以普通方式关闭定时器;5 F& t8 w) l% d, G% U* g7 k8 [( i5 s* L  W
参数 1:定时器句柄,根据实际需要填写;
6 u+ `. y% Y$ P- F& M6 Y返回:操作结果,返回 HAL_OK 或者 HAL_ERROR;# b. V% u! T  J6 Y" e
  1. HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim)
复制代码
功能:以中断方式打开定时器;
* `, |$ j: t6 U2 Q  K0 P* y! U参数 1:定时器句柄,根据实际需要填写;
& V0 O) _/ H9 g, r( s返回:操作结果,返回 HAL_OK 或者 HAL_ERROR;& M" \2 D! N, i- x# G
注意:用这种方式启动定时器时,定时器中断会被使能;
$ f) B8 l6 P3 }6 U
  1. HAL_StatusTypeDef HAL_TIM_Base_Stop_IT(TIM_HandleTypeDef *htim)
复制代码
功能:以中断方式关闭定时器;
8 A3 n+ M6 t0 u+ I9 n& q" }% R9 ]参数 1:定时器句柄,根据实际需要填写;( t4 V5 G5 B0 _* D) J* z9 }6 d
返回:操作结果,返回 HAL_OK 或者 HAL_ERROR;" w/ ~3 r& u8 l+ y6 e

5 v- l( l1 W7 j" \$ b; e! L
  J: k. G+ ]3 W( \8 }9 T4 B! X
定时器中断函数说明:
2 }+ h6 I/ n. H) ^* u
void TIM1_UP_TIM16_IRQHandler(void)3 V/ O: M* E8 `  Z, ^
{  V* C" ]4 V' v+ h) R3 k
HAL_TIM_IRQHandler(&htim1);1 h: k1 ^% L, U* ]
}3 X& X/ o3 }" F% i) q

; b% S7 q/ p) F: t功能:TIM1 更新中断和 TIM16 全局中断的处理函数;
3 l; q0 @! o$ M5 g5 ]1 L! f参数:无;" C8 V. F# Y* l1 M% A. B+ }$ V
返回:无;; C$ E! \5 }. c$ H
说明:当 TIM1 产生更新中断或 TIM16 产生任何中断时会进入此中断处理函数,这个函数调用相关的中断服务,最后调用对应的回调函数进行处理。- }2 H( C7 q4 W" [. @4 {# j
  1. __weak void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
复制代码
功能:定时器更新中断中断回调函数;
" b( f- c0 i8 U+ N" h参数:定时器句柄,由调用此回调函数的中断服务函数进行填写;
( A/ @5 p! r7 b, M+ k: N返回:无;2 d) c7 V7 H  y/ Z6 @  Z5 d" ]
说明:所有定时器更新中断回调函数都会调用此函数,因此在进入函数后需要对具体哪个定时器触发的更新中断进行判断。
1 G2 G2 [5 F- x1 I1 [2 N
6 ^& X, Q* A- q& l+ y

8 D+ R  \1 b- `% A" D9 B) x9 D核心代码:
7 V2 a# X! d7 x0 I
  1. HAL_TIM_Base_Start_IT(&htim1);//启动 TIM1
复制代码
在 main 函数完成外设的基本初始化之后需要以中断方式开启定时器才能正常工作。" W( S5 Z5 m$ z4 B+ ^3 m; P( [

' n2 l+ v. H) |# y$ g! a) d0 E# X

4 L, A: d- k7 K% @//定时器中断回调函数7 g1 M% W) Q" z/ G  K" J/ _
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)9 g8 m- E6 q; ]( I8 k, [
{4 r" {% z. K2 L( V
if(htim==(&htim1))
- Z' s: j; q& A# Q{
. N" i9 c8 ~3 J: t# ^, }" YHAL_GPIO_TogglePin(RGB_R_GPIO_Port, RGB_R_Pin);//TIM7 中断翻转红灯
% Q- p( c& }% j8 q+ v( h# A" Q' X; s}* b7 z6 ^- U3 z6 K% Z( }- B3 W
}) q; e6 X* Q7 X

* t4 G5 t' \- ~9 T
在定时器中断回调函数中判断是否是有 TIM1 更新中断触发的中断处理,然后翻转 RGB 灯的相应 IO。
, a5 F7 k9 X; E: j8 h9 S7 o3 I0 {* ]

+ E: o5 M2 r" y实验现象:- t7 M, P& \4 }3 t. [+ F  K) a
下载烧录后可以观察到 RGB 灯中的红灯以 1HZ 的频率闪烁,亮灭各 0.5s。
; U+ _- n/ V5 z9 _* y( U* k
7 \  X# p4 M9 A8 e- b
3 A( F  \" }) T6 d, o9 N: _7 M
2、单路 PWM 输出实验
7 u# v$ j3 ~% s4 n: J  d3 A; F' D% FCubeMX 配置如下,保存后生成对应的配置代码:- g5 U5 q% y6 A" R  x1 V1 n
微信图片_20241122131647.png
/ N. b. O2 _! {6 D& g0 T- V* x5 h
▲ 定时器 PWM 输出配置

6 L6 q6 E5 p7 N3 o9 [
本实验使用 TIM1 作为中断定时器,使用 TIM1 的 CH1 产生 PWM 输出,配置系统时钟为170MHz,具体配置见例 3.2.1,定时器设置计数周期为 1ms,即最后产生 PWM 周期为1ms,设置比较值为计数周期的一半,产生占空比为 50%的 PWM 波形。, ?- S4 ]7 i0 A
) d$ ~% X4 q- X5 j- O- m* p) z; J
* o- ~+ U% q; L' L2 m6 |
定时器 PWM 输出操作函数说明:
0 Z7 {0 ]5 b' {' |$ H: A
  1. HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel)
复制代码

; R, Y& l, B/ M) ~' l$ a% S) ?功能:以 PWM 输出方式启动定时器;$ ?( u( l+ u+ I3 s  R# o
参数 1:定时器句柄,根据实际需要填写;
( |% ?1 x/ i8 g4 v参数 2:输出通道,该值为 TIM_CHANNEL_X,X 为 1-6,根据实际需要填写;
* h+ l: |$ K8 u2 F  h( F1 I返回:操作结果,返回 HAL_OK 或者 HAL_ERROR;! S! F( \6 C# C5 w
  1. HAL_StatusTypeDef HAL_TIM_PWM_Stop(TIM_HandleTypeDef *htim, uint32_t Channel)
复制代码
) a3 C" ^9 w! R% }  d' y  ?* F
功能:停止响应定时器的制定通道的 PWM 输出;  J7 d- C1 B( q& A, `" F
参数 1:定时器句柄,根据实际需要填写;
/ _. a8 d9 d3 I' x  P0 g( o* G% X参数 2:输出通道,该值为 TIM_CHANNEL_X,X 为 1-6,根据实际需要填写;
' @7 z! J" U5 G; g) l1 i  G返回:操作结果,返回 HAL_OK 或者 HAL_ERROR;
3 `% y9 G2 Q; ~6 Z4 G! Z9 ~% t2 Y- u1 r4 l2 c

6 {7 h' E3 r$ J0 o6 L5 m  Z# r核心代码:  A4 [' Q8 q0 ]
  1. HAL_TIM_Base_Start_IT(&htim1);//启动 TIM1
复制代码
  g0 W% e- t/ S
在 main 函数完成外设的基本初始化之后需要开启定时器 PWM 输出才能正常工作。不需要单独开启定时器,直接调用此函数定时器自动开始运行。
; o/ V" O& |& l  H' Z6 K3 S! m- B  w, u9 h& n/ J5 `1 R4 w
7 P+ a+ |: \$ t, T2 y1 f0 _) J
实验现象
( u& X. M) Q2 X5 \) \. b% B  h+ u下载烧录后可以观察到 PC0 产生频率为 1kHz,占空比为 50%的方波。
; D- m' `0 c  Q/ q1 s( m, D
微信图片_20241122131645.png : l% Q; ^+ C& i% i+ U- I
▲ 单 PWM 输出实验现象
3、带死区互补 PWM 输出实验
CubeMX 配置如下,保存后生成对应的配置代码:# @" g5 T' @# F9 v, [. ^
微信图片_20241122131643.png
% `' F7 X% @; j
▲ 带死区互补 PWM 输出配置
本实验使用 TIM1 作为中断定时器,使用 TIM1 的 CH1 和 CH1N 产生互补 PWM 输出,配置系统时钟为 170MHz,具体配置见例 3.2.1,定时器设置计数周期为 1ms,即最后产生PWM 周期为 1ms,产生占空比为 50%的 PWM 波形,死区时间大概为 588us。
) C. I  P. ^  }7 _6 n
5 F1 a2 O9 e$ g5 c  T

+ b9 _2 R) C' S7 U相关操作函数说明:
6 {6 v" t0 }8 \$ Y
  1. HAL_StatusTypeDef HAL_TIMEx_PWMN_Start(TIM_HandleTypeDef *htim, uint32_t Channel)
复制代码

/ x! t" ?! X, i% S功能:开启反相通道的 PWM 输出;
* f3 E8 A+ l5 ?参数 1:定时器句柄,根据实际需要填写;
! ]9 `5 [; M  w; [参数 2:输出通道,该值为 TIM_CHANNEL_X,X 为 1-4,根据实际需要填写;% ]5 X* M. e& O. ]
返回:操作结果,返回 HAL_OK 或者 HAL_ERROR;& N% k% N5 j! r( w3 T9 O5 t
  1. HAL_StatusTypeDef HAL_TIMEx_PWMN_Stop(TIM_HandleTypeDef *htim, uint32_t Channel)
复制代码

. I% `  `* u/ m+ G, Z3 z功能:关闭相应反相通道的 PWM 输出;, B& ?3 n4 G" D, T" A( m
参数 1:定时器句柄,根据实际需要填写;" q/ Y& p5 K; \' b8 B1 J* S' z& a8 d
参数 2:输出通道,该值为 TIM_CHANNEL_X,X 为 1-4,根据实际需要填写;
5 R7 K. C- F1 Z7 T" w2 u返回:操作结果,返回 HAL_OK 或者 HAL_ERROR;
$ q, G. y1 N; _2 i
  1. __HAL_TIM_SET_AUTORELOAD(__HANDLE__, __AUTORELOAD__)
复制代码
7 ~6 i, x' B# j5 q. N
功能:修改定时器的周期自动装填值;
8 _, J, z$ l, @' X参数 1:定时器句柄,根据实际需要填写;  z7 r! N2 T" A8 p5 f8 f  j
参数 2:自动装填值;) A# y, M' ~( z" k4 H9 I: @/ g
返回:无;
5 n( l/ i& @# c7 \' |示例:__HAL_TIM_SET_AUTORELOAD ( &htim1,499 );//设置 TIM1 的自动重装填值为499,即修改计数周期% e8 v2 L; W/ D
说明:该函数是一个宏定义,实质上是直接通过句柄修改相应自动预装填寄存器的值,用来在程序中修改定时器的计数周期;3 E3 s+ e* w# J9 o) `% B" o
  1. __HAL_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__)
复制代码

0 X& j$ }! u8 _$ A功能:修改定时器的指定通道的比较寄存器值;
6 U# b! [! }4 D$ Q, p' y. o参数 1:定时器句柄,根据实际需要填写;+ C" S5 U) ^$ _" {  l
参数 2:定时器通道,该值为 TIM_CHANNEL_X,X 为 1-6,根据实际需要填写;
7 S; A8 F# \( M' ^0 x$ Y  @# h. @参数 3:要修改的比较寄存器值;/ _8 p7 `" t6 v. ]1 g5 _
返回:无;0 l) c+ H) o0 G; }
示例:__HAL_TIM_SET_COMPARE ( &htim1,TIM_CHANNEL_1,count_temp );//设置 TIM1的 CH1 的比较值
2 l/ k6 z2 e0 `说明:该函数是一个宏定义,实质上是直接通过句柄修改相应比较寄存器的值,用来在程序中修改定时器的 PWM 输出占空比等;
  c1 z% G* V; w2 ]) k
: y1 s+ D) F( a) ]/ q1 |

/ _% m3 N& H, |1 \% a  b1 c- i  }- x核心代码:
/ l; b, H8 A& r& x- e! c
  1. HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);//开启 CH1 的 PWM 输出- n0 z5 \8 K3 o; V4 B9 w9 p

  2. . \% j; M  C- L/ {1 N
  3. HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_1);//开启 CH1N 的 PWM 输出
复制代码
* D% d2 T+ E0 l9 _. d
在 main 函数完成外设的基本初始化之后需要开启定时器 PWM 输出才能正常工作。不需要单独开启定时器,直接调用此函数定时器自动开始运行。
1 t- q1 m! g' E4 T' P/ U! L+ _# ?4 U2 u' _' \# }9 s# s

4 `9 o5 E0 b) j2 H# _实验现象:
, h3 v- z7 G  `5 X1 K: K下载烧录后可以观察到 PC0 产生频率为 1kHz,占空比为 40%的方波,PC13 与 PC0 波形互补,即频率为 1kHz,占空比为 60%,两者之间有 588us 死区。5 f- Z, e4 b( E
微信图片_20241122131640.png
. T7 Z1 z$ R' I
▲ 互补 PWM
微信图片_20241122131637.png & p+ b0 Q7 d  f% \( h
▲ 信号死区

+ x  r4 F( }% E! u/ Q
9 l- t, ~+ m9 W2 \" \( E
4、定时器捕获测量频率占空比(中断方式)! d: ^1 O# r7 i$ Z% E6 z2 o
CubeMX 配置如下,保存后生成对应的配置代码:
. H2 {5 k1 |) [# \- G
微信图片_20241122131632.png
2 F8 b; J7 S" J. k8 O* U0 o1 i
▲ TIM2 输入捕获配置
微信图片_20241122131629.png 2 F1 h- ^# v# ?
▲ TIM2 中断使能
TIM1 的 CH1(PC0)输出 1kHz 的方波,TIM2 的 CH1(PA0)配置为输入捕获,采用中断方式进行测量,上升沿和下降沿都触发中断,在上升沿进行计数器复位并记录周期,下降沿记录脉宽,PC0 和 PA0 短接可简单测试,测量结果每 500ms 通过串口打印至 PC,串口配置使能 USART1 即可,不需要进行修改,串口打印需要进行 printf 重定向,具体见串口实验相关章节。
9 T! c* P- `% _8 D" F& d. b2 v- n- W; J  r0 z$ C5 [" C

; Z9 V+ T* W  w, o+ P相关操作函数说明:5 p5 D( i! Z5 m' D
  1. HAL_StatusTypeDef HAL_TIM_IC_Start_IT(TIM_HandleTypeDef *htim, uint32_t Channel)
复制代码

, r" S: j) ]: b' P8 r功能:以中断方式开启指定通道的输入捕获;1 O9 Z' m1 ^7 S6 {$ n$ K5 t. T
参数 1:定时器句柄,根据实际需要填写;
% h* U# l* \- ^* u参数 2:捕获通道,该值为 TIM_CHANNEL_X,X 为 1-4,根据实际需要填写;
! A, @4 u) B4 r返回:操作结果,返回 HAL_OK 或者 HAL_ERROR;1 z' M3 B6 B* _" {/ s# `, E6 e2 D) U3 P; Z
  1. HAL_StatusTypeDef HAL_TIM_IC_Stop_IT(TIM_HandleTypeDef *htim, uint32_t Channel)
复制代码
& C$ o/ e9 @' L
功能:关闭指定通道的输入捕获,停止相关中断;
3 V& H7 C: w8 q6 n; o参数 1:定时器句柄,根据实际需要填写;
) j5 r5 p+ p5 _7 w5 }' s7 H参数 2:捕获通道,该值为 TIM_CHANNEL_X,X 为 1-4,根据实际需要填写;
1 A1 Y& D2 ^7 E" d; ]$ p返回:操作结果,返回 HAL_OK 或者 HAL_ERROR;+ [" i" F, [  x
  1. __HAL_TIM_GET_COUNTER(__HANDLE__)
复制代码
* r; C$ Y0 [( y  {9 ~2 J
功能:获取指定定时器的计数值;' @/ k( W, M3 h; N3 m# N# q* G
参数 1:定时器句柄,根据实际需要填写;& i" h" [8 H& d9 u+ O) |
返回:无;( {3 [8 l, Y& M3 s: r
示例:Tim2_Count_rise = __HAL_TIM_GET_COUNTER ( &htim2 );//记录上升沿时的计数器数据,作为周期+ w) k' @2 K% [! Z6 W( k2 c
  1. __HAL_TIM_SET_COUNTER(__HANDLE__, __COUNTER__)
复制代码
功能:设置指定定时器的计数值;; y/ ^% [& k% H/ L
参数 1:定时器句柄,根据实际需要填写;( z7 {$ X' p$ l' ~
参数 2:要设定的计数值;
) h8 Q: M0 h- J- q. }返回:无;" U1 n0 N/ n; G4 m1 B3 q
示例:__HAL_TIM_SET_COUNTER ( &htim2,0 );//计数器复位清零
/ }$ F2 H$ }9 ]6 p& ^! m1 p( m8 E7 g8 C
$ B# N8 \0 v# i4 e
功能:TIM2 全局中断处理函数;
4 K& |) Y5 R  p' P( E参数 1:无;% J/ S2 R, p: f+ [" K0 ]$ X
返回:无;) a8 X5 ~8 W6 g6 M
说明:当 TIM2 产生中断会进入这个函数,这个函数对中断进行相应处理,最后调用相应的中断回调函数;
0 u1 _9 T( E1 i  l* x
  1. __weak void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
复制代码

7 E4 b3 O9 c) u2 V功能:定时器输入捕获回调函数;
% \6 {# K4 q! r; i5 f- k  t参数 1:定时器句柄,由调用该回调函数的中断处理程序填写;0 }6 ]/ C/ \: c' ^) X5 ?  q
返回:无;说明:该函数为所有定时器捕获触发中断的中断回调函数,所以进入该函数首先需要判断是哪个定时器产生的捕获中断,然后需要判断是哪个边沿产生的中断,以便于进行相应的处理;
* _" _  V7 \" h  P* P! {# e+ K$ `- [0 p& z& {& V% j& W

/ B( ~" ?0 S- A( z核心代码:
. w4 H6 a' c: ~% x
HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);//开启 CH1 的 PWM 输出' z$ Z/ K& Q: R2 M( t
HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);//开启输入捕获
0 D! o* j6 e4 i# hwhile (1)& L) ~$ i4 Y5 _0 d
{
' b% A# D; ?/ x0 V0 ^HAL_Delay(500);
1 e3 N4 m/ L7 `* H% f$ S. {Frq = (float)10000000/(Tim2_Count_rise+1);//计算频率  C- w$ [% ]/ A9 N/ Q
Duty = (float)(Tim2_Count_fall+1)/(Tim2_Count_rise+1)*100;//计算占空比) A6 l2 o4 F( e2 T1 @
printf("Frq:%.1f Hz Duty:%2.1f %% \r\n",Frq,Duty);//打印结果) c6 a! v' F' U, ?# J) l
}

  Z7 W7 [: T. q4 c3 x- n. R
, Z3 {$ E  \3 E9 P
0 P4 `+ x: P+ P8 r2 X
此部分为 main 函数中的启动部分以及进行相应的频率和占空比计算并进行打印操作。( ~7 U1 ^1 U5 r+ C) O  G
# J* t$ H- z4 z  Z* A
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim); U7 \. ^, M( l, N% z  ]4 W
{
) S! C  j( U# [if(htim == &htim2)
* A' |) P( n6 w0 w; `- P{
5 O) R) T) h* e* I6 X; |if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0))//上升沿触发) H% h# f" \6 J7 ~8 }* C. T& l
{
8 x" k$ F& o0 f, O# @  Z; I, iTim2_Count_rise = __HAL_TIM_GetCounter(&htim2);//记录上升沿时的计数器数据,作为周期  K$ x) o3 u, m$ @' m4 x
__HAL_TIM_SetCounter(&htim2,0);//计数器复位清零7 k7 ?0 b# z9 d4 X2 E2 l7 F4 z3 k
}& g( i& @9 z( B) m5 ^) a
else//下降沿
" W! j, o8 Q2 ?+ [{* `& t/ S6 H& l: z* e  m6 @+ U
Tim2_Count_fall = __HAL_TIM_GetCounter(&htim2);//记录下降沿计数器数据,测量脉宽: H$ G0 x! l, u2 l  o' C
}
# Z+ l9 F& O! m" R, @}
1 g* N  G  \, R  [}5 _0 |8 `6 A! J! m7 q# [1 J9 V
; M% k$ `  D, B$ I" {9 V0 _5 k# P! i

5 G& f/ ~+ }6 K& o9 ~5 X此部分输入捕获的中断回调函数,在此函数中进行了中断来源判断,相应边沿捕获值记录的操作。

$ s6 w$ N% J: X7 f" u! O0 E2 j9 X/ [5 ~

% K, v8 s9 O. W- q; h  p
4 x; i4 x) c4 @# G4 U7 T9 @
实验现象:
( [( N' h9 a* _# N# J3 v下载烧录后短接 PA0 和 PC0,通过 USB 连接 DAP 和电脑,在电脑端使用串口助手连接,可以观察到每 500ms 打印一次采集到的频率和占空比数据。, @' [" |7 |$ x. w2 y

9 I# W% v) t/ c+ k3 o+ |  u2 z% q% s五、串口实验+ F8 E+ t& C: N1 A8 @
实验目的:掌握和熟悉串口发送、串口接收以及串口 DMA 的使用和配置方法,掌握串口重定向的操作。
) A- x3 x8 X. q" c) x3 T) |5 ]) \* U  j3 s% V
1、串口查询方式收发实验; G3 W* o8 b. M- O& w  c& z
CubeMX 配置如下,保存后生成对应的配置代码:
6 R# M. t7 W7 a. G$ D: R; B. C: E
微信图片_20241122131625.png ! l2 F4 O, Q: R1 N# d, Z# q8 R

; O! a. X! D) B. v$ ]# n- K# j
▲ 串口配置
9 V- g5 b' A& q  s; B4 H8 l5 s. g6 _
本实验使用 USART1 的异步模式进行串口收发,保持基本配置,只需使能串口即可,时钟仍为 170MHz,具体配置见前文。

, x! {6 l- l; R; n

0 h& ?$ N9 K  G5 Z, |

5 P/ H8 j2 N, w( R
) n9 w: N  I  g2 N
相关操作函数说明:

$ m% v: w, o+ c1 O6 N7 z1 D% `$ ?
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout)
+ N  m* q+ W) h7 f2 K- n3 e3 I

0 E) h+ u  r4 G6 Z$ W  m/ ~
: P' \4 q" I4 E8 H& D
% Y( X. s7 a6 z) ~
功能:阻塞模式通过串口发送一定量数据;
& A! c) L9 T/ s! n% T
参数 1:串口句柄,根据实际需要填写;

9 A8 d! o# m1 B9 x: o
参数 2:要发送数据的首地址;

2 n- Z& P9 w) i4 {
参数 3:发送数据长度,单位字节;
' j+ s6 H+ V% l8 O3 R
参数 4:超时时间,单位 ms;示例:HAL_UART_Transmit ( &huart1,&UART_temp,1,1 );//通过 UART1 发送一个字节数据,超时时间为 1ms;
8 U3 @; }: ?4 f4 a
回:操作结果,返回 HAL_OK,HAL_ERROR,HAL_BUSY,HAL_TIMEOUT;
" m4 S( f2 I8 Z7 {) Z! U
说明:此函数为阻塞模式发送,调用该函数之后 CPU 会被占用,开始发送数据,直到所有数据发送完成,或者达到超时时间仍未发送完成,才会退出该函数;

- i* w$ S, N" ], ?$ i: x( }1 ?# |
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData,uint16_t Size, uint32_t Timeout)
% }, a) b& S) ^

8 s# o; r+ d9 ?2 e

: V! ]$ t5 }9 U( t
! K1 e$ s# m1 s8 F
功能:查询模式通过串口接收一定量数据;
( g- z8 v/ q# m+ K: U
参数 1:串口句柄,根据实际需要填写;

; Y& z* ]7 f" E5 J& [' q& x( m
参数 2:要接收数据存放位置的首地址;
0 n+ o% a1 A& H1 j) L
参数 3:接收数据长度,单位字节;

/ J# B8 S2 z* A1 _
参数 4:超时时间,单位 ms;
1 m, q- D7 l; T1 @2 ^# W" B
示例:HAL_UART_Receive ( &huart1,&UART_temp,1,1 );//通过 UART1 接收一个字节数据,超时时间为 1ms;

7 M" |$ f, W, {; K' v
返回:操作结果,返回 HAL_OK,HAL_ERROR,HAL_BUSY,HAL_TIMEOUT;

: _6 G- d; V7 D0 J$ {1 H
说明:此函数为阻塞模式查询接收,调用该函数之后 CPU 会被占用,等待串口接收到数据,直到接收到所设长度的数据,或者达到超时时间仍未接收完成,才会退出该函数;

/ k5 `+ O, O2 |7 R& n( n: [* d% q
% w# G* L& Q4 J5 ?' h: J5 z& b, \
核心代码:

  }' ?7 ]8 r& N7 v1 O* ^$ T, ~
while (1)
. N! D7 I4 I9 m$ F
{
+ r9 u* z0 a* `4 E  F! B7 F) D- m$ o
if(HAL_UART_Receive(&huart1,&UART_temp,1,1) == HAL_OK)//查询接收到数据
" o1 J5 O9 A+ t1 f" p4 V
{
  n" Z9 n5 u) M/ i$ m. s
HAL_UART_Transmit(&huart1,&UART_temp,1,1);//将数据通过串口转发回去

. K% v; F+ @3 |6 K% e6 |4 Y. x: i& u
}

: K1 I! g- e: p! S6 A( {; X6 Z) b* \
}

4 s& I2 ]1 z  P' O! v
2 g, o! A5 O8 c% o/ t

3 V! Z5 w! @5 L( Y
9 _9 Z* W; o( H: ^% P
在 main 函数完成外设的基本初始化之后,在 while(1)中不断查询是否接收到数据,如果接收到数据就通过串口在转发回去。
* V) r! [1 W0 |; w9 |
" @; P" Q. h! S: ~0 C  F
实验现象:
, g+ l. Q- F4 ~# n! T
下载烧录后可以观察到上位机发送任何信息,串口都会直接转发回来。

5 c) |% x( m- U& T) R0 L

. z1 {! k- j* g8 a" c
微信图片_20241122131617.png 0 D4 v. B9 y! ]: F& O8 S% q

4 U+ i8 P; d# a! T( s* [3 `
▲ 上位机收发数据

! _& ?) z8 N) r; [  c9 L7 y# }9 m* I% I8 _
2、串口中断方式收发实验
# h* Z0 l( m. q. A2 `
CubeMX 配置如下,保存后生成对应的配置代码:

/ H9 W& H7 f+ L; C9 [9 ]

% x4 x2 j; E) R# a* A
微信图片_20241122131613.png
5 H* r! |6 v5 t4 J! C) x
3 B8 W' q7 N' o" {
▲ 中断配置
5 o2 L. J$ ?  x' q7 S% G
本实验使用 USART1 的异步模式进行串口收发,保持基本配置,基本配置与上面相同,这里开启 UART1 的全局中断以进行中断收发。

, J) G2 j" p/ N( f! O4 O- }' ?. n
相关操作函数说明:

9 c8 K; r2 n; p  Y/ G0 \# u
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t*pData, uint16_t Size)

2 ]+ K7 S: P/ Z3 D
功能:中断模式通过串口发送一定量数据;

' g1 \+ N( P1 }! G/ Z  }7 s
参数 1:串口句柄,根据实际需要填写;
' h) s8 T1 c. |, B' ^
参数 2:要发送数据的首地址;

! }' V( l0 K, O' F7 n+ S4 {, c% O
参数 3:发送数据长度,单位字节;示例:HAL_UART_Transmit_IT ( &huart1,&UART_temp,1 );//通过 UART1 中断模式发送一个字节数据;
/ y# L8 x: b7 i: x7 R$ l
返回:操作结果,返回 HAL_OK,HAL_ERROR,HAL_BUSY;

, t3 O2 f: @  X8 ?
说明:此函数为中断模式发送,调用该函数之后 CPU 不会被占用,开始发送数据,直到所有数据发送完成,发送过程中以及发送完成之后如果使能了相关中断将会产生中断信号,调用相应的中断回调函数;
9 q7 ]9 |9 i$ B9 m
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData,uint16_t Size)
6 p9 h) p. Y- o/ F  y
) w, ^1 C7 U5 [  X
7 L2 b, t8 z: J8 b" R+ R7 v: S
" Q% V2 U0 C: A2 l" {
功能:中断模式通过串口接收一定量数据;
7 `$ `% n8 _( x. W, p1 R7 A, B' k
参数 1:串口句柄,根据实际需要填写;
$ ^- L+ P/ `; S# L. F( N7 h& M* T( z8 c
参数 2:要接收数据存放位置的首地址;

/ w0 z  l% r6 n- w' a8 l
参数 3:接收数据长度,单位字节;
  J, c, Z7 W2 @8 `& o3 r/ P9 Q
示例:HAL_UART_Receive_IT ( &huart1,&UART_temp,1 );//通过 UART1 接收一个字节数据;

: D& A1 u$ R1 F+ J. @* t+ j" V9 Z
返回:操作结果,返回 HAL_OK,HAL_ERROR,HAL_BUSY;
' {5 V- c& X6 V2 u3 C( A
说明:此函数为中断模式查询接收,调用该函数之后 CPU 不会被占用,等待串口接收到数据,直到接收到所设长度的数据,或者其他相关事件会触发相应中断,调用相应中断回调函数;
( K; }8 Y: I3 Q7 Q' O

' ]( c% ^. x' z
核心代码:
5 c7 A  K' z3 Z3 ]/ v
HAL_UART_Receive_IT(&huart1,&uart_temp,1);//中断模式开启 UART1 接收,接收 1 字节。
7 V3 U1 J! j9 c- v$ j7 Z

# t7 C3 Z* v1 u2 z4 X
/ D: |( M3 n# v7 c! d$ i8 t9 V1 j8 c$ j& [5 V9 v7 {* w! U% a
在 main 函数完成外设的基本初始化之后,开启串口中断接收。
8 |1 C' C  i( T, j0 \# v( B( O2 |
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
* s- y9 Q, L) `  V* r. o
{
5 d" K! G/ n3 D9 r
if(huart == &huart1)
* U8 A2 r1 x  w( v5 ?. }) W) d4 |
{
6 Z  W4 e+ G% N7 o; D
HAL_UART_Transmit(&huart1,&uart_temp,1,1);
0 C/ T& P' u# k
HAL_UART_Receive_IT(&huart1,&uart_temp,1);

" ^2 s4 C+ D% d* Q1 ~! c
}

3 M" e6 v: {- Y; C, ~/ S
}
# s, Z1 ]  c2 ~' o3 N  u0 `

) \, R  g. V& h& u$ a; @! H, L, S2 ~$ C  J0 n% W+ c

8 d  i) X" ^0 Q9 a7 z
中断回调函数,在接收完成之后判断中断来源,中断中将接收到的数据转发出去并重启接收,注意如果不重启接收之后不会再收到数据。
- S4 g0 E+ N9 H( O& f

; b: o/ i. a5 L; ]' t
实验现象:
" A$ @$ m  i4 @# P
下载烧录后可以观察到上位机发送任何信息,串口都会直接转发回来。
% r$ D" N; A: g
# X: W0 S. m* Y( r/ {
微信图片_20241122131611.png
% d, @" Q. f" l$ {- x/ e9 @* w
6 a; T& f; s: B5 f
▲ 上位机收发数据
4 r3 T. f1 u) f+ y. S* G0 E

* G. k* E( W( X, Y* W* e; H( s0 [6 ?
3、串口 printf 重定向打印实验

2 L. u1 V9 m/ ?  z$ O8 r
Keil 配置如下,保存后生成对应的配置代码:

# |. Q! r4 _2 W; Q
) W1 c* X& P6 `- t7 A  a$ \
微信图片_20241122131608.png ( Y0 t, R3 ~; Q4 e. i4 J3 i) P. J

+ T* s- j5 z9 J. V( ?3 t/ |
▲ 配置 keil 使用微库
2 M' k/ d2 @3 Y# D8 z( Z2 N8 w
本实验使用 USART1 的异步模式进行串口收发,保持基本配置,这里需要对 keil 进行配置。
# |7 ?, D. y9 ^& q6 g

* n0 C3 l& M: K
% f) L6 B% M" m. G7 {& e5 L% d6 y" v! }* l5 W
相关操作函数说明:
; G& b0 v  g$ A3 e# g
int printf ( const char * format, ... )

/ [3 Q, H" q; ?5 `6 w. Z- Y1 Q- n# u* w/ H% K" c

  E$ _( n* ]' t; O2 Q
% I2 p6 k* g7 i1 s
功能:格式化并打印字符串;
* D6 B+ n: W) N
示例:printf("this is uart1 test");//输出字符串;
' L0 N0 d1 Q0 D& W. ^: D( z
说明:此函数为 C 语言标准库函数,具体详细用法参照 C 语言,使用时需包含头文件stdio.h;
) S4 c9 J; n5 T2 f- D& F

) Q# k4 t$ H' h& ^) e
( p: w/ F1 e3 v1 n5 f
" ^+ {* m3 j( \4 }$ b' n
核心代码:
+ w6 D4 F7 P) H3 \, e2 b0 H9 ~2 Q
//重定向 printf
) p0 D; `2 N& x  K( y
#include "stdio.h"

7 @7 x, ?. k  Z1 g8 d
int fputc(int ch,FILE *f)

8 z- [* J& Z# ?9 a1 q
{
4 F: \( ~. z- U: _  B
uint32_t temp = ch;

, T: O  W8 O: h# b# K' d/ A# k
HAL_UART_Transmit(&huart1,(uint8_t *)&temp,1,1000);

) b! |& G& h( {& |6 @' K
while(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_TC)!=SET);
/ z  e! i' F' w$ x  B* T
//等待发送
+ G, |0 D5 f4 t. v6 Z# c& h
结束
3 |  n* e3 z/ ?: Y0 z
return ch;

1 ?. Y- \7 f! ?8 g! i
}

( F1 N7 W+ o+ O- k
: x6 |9 _- b5 v# \
1 I3 u3 `$ }6 m, x8 G: [
! T! F" c7 N( ~0 x% e
在 main 函数前对 fputc 进行重定向,使用串口进行相应的发送操作。

6 \+ ~/ p$ I5 e; P
while (1)
  h% J  B7 e. \2 G+ l0 Q, C; x) Q+ Y
{

1 y. |, N! S8 D
printf("this is uart1 test");//打印测试内容

5 H4 \" v  S/ o: {
HAL_Delay(1000);//延时

8 k$ B, T2 s' X" G3 r- _
}

( l& K* v* Q/ T! R1 E& c5 s3 p' _7 e3 _
- j- a& \* L* C" h- c
" \! q& p/ ?, h
8 v; U6 a* b' {, g: n5 L9 x
main 函数中的循环函数,每 1s 发送一次字符串 this is uart1 test。

" U- q7 d0 L; R+ S
9 s) }" I$ T9 ?% E  ]
* L+ s4 R0 J% x% j
, k/ L0 m0 y2 M8 z
实验现象:
: a0 @/ Q. A- ^4 o
下载烧录后可以观察到上位机串口助手每秒收到一次信息。

' G: C% K. j$ Y- D4 l+ j/ q# L3 L9 y

" }+ @3 v. n% e% F/ N* O1 z# J1 p
微信图片_20241122131605.png 6 r' n1 g7 ?2 v) l6 J

: A& g! x2 `0 e, E: L# ?
▲ 上位机每秒收到一次数据

5 ]# c( O& N* d1 _, W
7 C' O/ m, {" l3 ]1 {
1 z+ i3 L, x, B* O, m9 }
) J, j! z4 _- `5 K/ g) s
4、串口 DMA 收发不定长数据实验

7 m" r2 y$ N6 o8 `2 ?+ {) P9 T2 ~
CubeMX 配置如下,保存后生成对应的配置代码:

9 w1 F0 J9 t) A0 u3 G
+ W8 S# u; S# v
微信图片_20241122131558.png 6 j  _( {) n- U$ f1 M0 P  V

$ N7 O% _* F9 D, L4 r. W8 [
▲ DMA 配置

' f8 d; y) J. R
微信图片_20241122131554.png 5 c! q- |6 Y/ q; u1 T( \
1 B; j9 f- O0 [" F2 Q
▲ 中断配置

  @) M$ [& v! m
本实验使用 USART1 的异步模式进行串口收发,保持基本配置这里开启 DMA 进行收发,使用普通模式即可。
7 ^  v0 w4 _! t2 _# s' n
4 j; M2 G$ k* |) w8 d
! S$ w/ y$ q8 i: h* E7 a9 A

  B! T: R/ O, |) A3 [- T
相关操作函数说明:

# h! `* Z; H! K1 e& e
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)

7 N% G8 J; h$ N1 s: f3 h7 t& ]
功能:DMA 模式通过串口发送一定量数据;
" h& Z6 F2 x" t4 q8 u# Y. z
参数 1:串口句柄,根据实际需要填写;
$ S3 d+ g: r6 U' \; e
参数 2:要发送数据的首地址;

' `. t, R+ |& f8 Q
参数 3:发送数据长度,单位字节;

1 Q( N! q0 Z8 {: P( H& V, H8 T; ^
示例:HAL_UART_Transmit_DMA ( &huart1,UART_temp,10 );//通过 UART1 中断模式发送 10个字节数据;
6 f, c5 g1 {. I# @
返回:操作结果,返回 HAL_OK,HAL_ERROR,HAL_BUSY;

7 t1 K5 Y. u2 C: `1 [' k2 W1 [
说明:此函数为 DMA 模式发送,调用该函数之后 CPU 不会被占用,由 DMA 进行相关数据发送搬运,发送过程中以及发送完成之后如果使能了相关中断将会产生中断信号,调用相应的中断回调函数;

  W4 {1 }, A$ n1 Q2 t
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t*pData, uint16_t Size)
8 r  A0 h: A" B7 Y: B) J

0 Q3 k% p2 a* }6 _, Y, ~; o2 P8 W: A" ]7 Z

* P/ V+ j$ i+ }/ e
功能:DMA 模式通过串口接收一定量数据;
1 B  l" m4 a2 ~% W3 }. ]
参数 1:串口句柄,根据实际需要填写;

: \1 I+ v+ N! E, Q
参数 2:要接收数据存放位置的首地址;
. K( O2 O- u7 W* O( G
参数 3:接收数据长度,单位字节;
( Z/ E$ e, I- J: o9 C: f5 F- b! d
示例:HAL_UART_Receive_DMA ( &huart1,UART_temp,10 );//通过 UART1 接收 10 个字节数据;

; I" ~9 B! z8 b0 q2 a$ d+ [
返回:操作结果,返回 HAL_OK,HAL_ERROR,HAL_BUSY;

9 |2 h+ G# b& ^$ |4 U
说明:此函数为 DMA 模式查询接收,调用该函数之后 CPU 不会被占用,接收到的数据由DMA 搬运到内存,不需要 CPU 参与,等待串口接收到数据,直到接收到所设长度的数据,或者其他相关事件会触发相应中断,调用相应中断回调函数;

. d' N* e1 v0 O2 R6 o$ ?
____HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__)

1 T9 N% m7 D0 M- T* f8 O
功能:使能 UART 指定中断;
) Q& E# e6 E4 l* y2 t# }1 i: |
参数 1:UART 句柄,根据实际需要填写;

/ V8 \; l# f. `$ o8 P# h
参数 2:中断类型,可选:UART_IT_RXFF、UART_IT_TXFE、UART_IT_RXFT、UART_IT_TXFT、UART_IT_CM、UART_IT_WUF、UART_IT_CTS、UART_IT_LBD、UART_IT_TXE、UART_IT_TXFNF、UART_IT_TC、UART_IT_RXNE、UART_IT_RXFNE、UART_IT_RTO、UART_IT_IDLE、UART_IT_PE、UART_IT_ERR;
5 b0 f% j9 }/ v1 W0 ?5 L
返回:无;

' Z  ?4 Q' T1 v/ @
说明:有些中断,比如空闲中断,HAL 库没有直接提供以此中断方式启动的函数,因此需要手动开启这些中断。

# P: {7 j% |* N; }( F/ f, ~$ y$ t/ [4 |3 p

8 D' y. i- j* S1 ], @& h6 J" H- D
& j: q2 H+ E# q& k0 r! R
核心代码:
3 t  @% A7 w, D7 }3 _
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 使能串口空闲中断
+ T' H) h# t; C" w6 K& N7 T* [# I
HAL_UART_Receive_DMA(&huart1, (uint8_t*)receive_buff, 255);//开启 DMA 接收

3 m9 D) j$ h+ a' d0 E; P4 S/ q  G# u7 {; m2 ~8 W
0 e: t  e' b& z8 q) c
3 [3 H2 B8 w5 ^: N; P/ ^+ x
在 main 函数完成外设的基本初始化之后,开启串口 DMA 接收,DMA 接收数组长度应长于预期接收最大数据数量,手动开启空闲中断。
. u! l6 E+ G% v5 }4 F% S! f; W- k

) s) \$ x+ E: v$ t& ~- |
5 o5 ~5 R; M; n8 M' Q5 f% t$ x
5 x- }# z/ T! K9 M& ^% R
void USART1_IRQHandler(void)
9 I2 p/ _0 p/ P7 i' ?9 D
{

' a* J8 G; d' Q& n) i+ F" @
/* USER CODE BEGIN USART1_IRQn 0 */

  _/ j' ]  e" c$ ^! {* o8 h4 S
//串口空闲中断处理
7 ~0 V, p: F. |; ?6 S
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))//进入了空闲中断
/ W. O: a  Z4 E8 V4 X
{
( l0 Y$ D7 c5 l! n' M. H, w! _
__HAL_UART_CLEAR_IDLEFLAG(&huart1); //清除空闲中断标志

: t# v* ~3 O2 Y- Y2 _( m! {
HAL_UART_DMAStop(&huart1); //停止 DMA 接收
4 m9 d% \; |7 C3 }. a; I9 R
uint8_t data_length = 255 - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); //计算接收到的数据长度

* N/ w1 p  [& l: ?2 \' {% [
printf("RX data len is %d \r\n",data_length);
/ G$ d- s0 M& J% o7 T
HAL_UART_Transmit_DMA(&huart1, receive_buff, data_length); //将收到的数据转发出去
  K7 c! i' C; R2 z
HAL_UART_Receive_DMA(&huart1, (uint8_t*)receive_buff, 255);//重启 DMA 接收

9 g. O) E0 O) r! r
}

" z7 P1 O9 H. s& ]7 s' m( b
HAL_UART_IRQHandler(&huart1);

# M/ ~3 z6 z8 e- C' o
}
3 Q2 y0 x2 J! q. w6 R* S% f( t( w

1 k: s/ ?6 \" D2 s! Z& r) d+ `5 t6 M7 D* N5 r
2 D1 n$ Y( @8 y( }
Stm32g4xx_it.c 文件中的 USART1 中断服务函数,由于空闲中断没有相应回调,所以需要在中断服务函数中添加代码,进入中断判断是否为空闲中断,如果是空闲中断则说明这次数据已经接收完成,此时关闭 DMA 接收,将接收到的数据进行处理,发送,最后重启新的一轮 DMA 接收。
6 T3 ~3 d5 V5 {

6 z8 W* U2 H! N& l# _, _' }# P% q8 ?

0 A' x! X1 f3 S0 V6 b+ e# F$ K
实验现象:
9 t# }& N3 g' `7 D
下载烧录后可以观察到上位机串口助手下发信息会转发回来,并且发送接收到数据的统计信息。
2 o4 Y5 m& O- J7 _5 R
* u% |0 e- n! L1 H( @
微信图片_20241122131544.png . y/ P, F) i% t6 Q7 i

' C4 o4 m4 b3 ]1 M. h/ c% q
▲ 上位机收发数据

9 u7 `/ I) u! A7 \+ [/ v! X! C
如有侵权请联系删除

2 {- E* I6 W% N/ r
转载自: AI电堂
  J' L. f) A4 }/ J) T4 b8 T
4 Q+ U% k7 I& H8 S

% N. ^. p) J# C  U' I
! o  B5 J, p* w2 m0 T
. s# |( S4 o. X9 g+ l. Y9 I2 ^/ W6 O  a+ p0 n( ?- [+ g5 B' l0 o4 f) g
  O4 F# c- v7 J, f3 o; R8 b- o  ?
( \/ ]7 r! s, Y. _, y. n( q
" S  v6 `3 C4 @" X8 L9 C
收藏 评论0 发布时间:2024-11-22 14:02

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版