STM32F1 开源代码阅读笔记《持续更新中。。。。》

一、STM32 PWM控制呼吸灯实验代码阅读

1、SysTick定时器简介:

(1)介绍:

SYSTick定时器也叫SYSTick滴答定时器,它是Cortex-M3内核的一个外设,被嵌入在NVIC中。它是一个24位向下递减的定时器,每计数一次所需时间为1/SYSTICK(SYSTICK是系统定时器时钟,它可以直接取自系统时钟,还可以通过系统时钟8分频后获取)。当定时器计数到0时,将从LOAD寄存器中自动重装定时器初值,重新向下递减计数,如此循环往复。如果开启SysTick中断的话,当定时器计数到0,将产生一个中断信号。因此只要知道计数的次数就可以准确得到它的延时时间。

(2)定时器寄存器:

CTRL寄存器

CTRL是SYSTick定时器的控制及状态寄存器。其相应功能如下:

 countflag:计数

clksource:时钟源。0表示分频后,1表示内部系统时钟

tickint:中断时开启

enable:使能位

LOAD寄存器

LOAD是SYSTick定时器的重装载数值寄存器。其相应功能如下:

VAL寄存器

VAL是SYSTick定时器的当前数值寄存器。其相应功能如下:

 

 current:可用于获取寄存器计数状态。

CALIB寄存器

CALIB是SYSTick定时器的校准数值寄存器。其相应功能如下:

(3)SYSTick定时器配置步骤:

分为四步:

(1)设置时钟源;

(2)设置重装初始值(如果要用中断的话,将中断使能打开);

(3)清零当前计数器的值(防止之前用过定时器的值未被清除);

(4)打开定时器。(使能位)

2、SysTick系统定时器编程:

SysTick_Init(72)中72表示72MHz系统时钟。

采用系统函数8分频。

fac_us:表示us延时倍乘数;

fac_ms:表示ms延时倍乘数。 

注意:delay_ms(nms)最多延时1864ms(1.864s),如需更长时间需要多次使用。delay_us同理。

2、中断系统

(1)中断介绍:

CPU执行程序时,由于发生了某种随机的事件(外部或内部),引起CPU暂时中断正在运行的程序,转去执行一段特殊的服务程序(中断服务子程序或中断处理程序),以处理该事件,该事件处理完后又返回被中断的程序继续执行,这一过程称为中断。引发中断的称为中断源。

 (2)NVIC介绍:

NVIC英文全称是Nested Vectored Interrupt Controller,中文意思就是嵌套向量中断控制器,它属于M3内核的一个外设,控制着芯片的中断相关功能。由于ARMNVIC预留了非常多的功能,但对于使用M3内核设计芯片的公司可能就不需要这么多功能,于是就需要在NVIC上裁剪。ST公司的STM32F103芯片内部中断数量就是NVIC裁剪后的结果。

中断控制相关寄存器在固件库core_cm3.h文件NVIC结构体内。可打开任意库函数工程即可查看到。

(3)中断优先级:

STM32F103芯片支持60个可屏蔽中断通道,每个中断通道都具备自己的中断优先级控制字节(8位,但是STM32F103中只使用4位,高4位有效),用于表达优先级的高4位又被为组成抢占式优先级响应优先级,通常也把响应优先级称为“亚优先级”或“副优先级”,每个中断源都需要被指定这两种优先级。

高抢占式优先级的中断事件会打断当前的主程序或者中断程序运行,俗称中断嵌套(抢占式优先级不同)。在抢占式优先级相同的情况下,高响应优先级的中断优先被响应。

当两个中断源的抢占式优先级相同时,这两个中断将没有嵌套关系,当一个中断到来后,如果正在处理另一个中断,这个后到来的中断就要等到前一个中断处理完之后才能被处理。如果这两个中断同时到达,则中断控制器根据他们的响应优先级高低来决定先处理哪一个;如果他们的抢占式优先级和响应优先级都相等,则根据他们在中断表中的排位顺序决定先处理那一个。

STM32F103中指定中断优先级的寄存器位有4位,这4位的分组方式如下:

设置优先级分组可调用库函数 NVIC_PriorityGroupConfig(),在misc.c可以查看。

(4)中断配置步骤:

要使用中断我们就需要先配置它,通常都需经过这几步:

(1)使能外设某个中断(有专门的库函数进行配置);

(2)设置中断优先级分组,初始化 NVIC_InitTypeDef 结构体(misc.c)

typedef struct
{
  uint8_t NVIC_IRQChannel;                    //中断源
  uint8_t NVIC_IRQChannelPreemptionPriority;  //抢占优先级
  uint8_t NVIC_IRQChannelSubPriority;         //响应优先级
  FunctionalState NVIC_IRQChannelCmd;         //中断使能或失能   
} NVIC_InitTypeDef;

(3)编写中断服务函数(有固定函数名,在启动文件里可以查找,不要修改)。

(5)外部中断:

(1)外部中断介绍

EXTI简介

STM32F10x外部中断/事件控制器(EXTI)包含多达 20 个用于产生事件/中断请求的边沿检测器。EXTI的每根输入线都可单独进行配置,以选择类型(中断或事件)和相应的触发事件(上升沿触发、下降沿触发或边沿触发),还可独立地被屏蔽。

(视频中了解到EXTI类似于是外部按键触发外部中断,这个还挺有利于我们做一些外部按钮啥的)

EXTI结构框图(重要,掌握有利于软件编程思路)

1-->2-->3-->4-->5:这条路线是控制NVIC中断进入执行中断服务函数;

1-->2-->3-->6-->7-->8:这条线是产生脉冲信号到外设,我的理解是做触发信号使用(触发定时器或ADC转换等)。

注意到EXTI时钟源是PCLK2,也就是APB2总线提供,所以使能EXTI挂接在APB2,要开启其时钟。

(6)EXTI配置步骤:

(1)外部中断、事件线映射

STM32F10x的EXTI具有20个中断/事件线,如下:

 要是用AFIO来进行EXTI的配置:

#注意:PA0,PB0,PC0 、、、PG0都是对应EXTI0,同理其他。

 (2)外部中断配置步骤

要使用外部中断我们就需要先配置它,通常都需经过这几步:(EXTI相关库函数在stm32f10x_exti.c和stm32f10x_exti.h文件中)

(1)使能IO口时钟(基本套路),配置IO口模式为输入;

(2)开启AFIO时钟,设置IO口与中断线的映射关系:RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);

void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);

例如:GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);

(3)配置中断分组(NVIC),使能中断。

(4)初始化EXTI,选择触发方式:

void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);

typedef struct

{

  uint32_t EXTI_Line;               //中断/事件线

  EXTIMode_TypeDef EXTI_Mode;       //EXTI模式

  EXTITrigger_TypeDef EXTI_Trigger; //EXTI触发方式

  FunctionalState EXTI_LineCmd;     //中断线使能或失能

}EXTI_InitTypeDef;

(5)编写EXTI中断服务程序:

EXTI0_IRQHandler

EXTI1_IRQHandler

EXTI2_IRQHandler

EXTI3_IRQHandler

EXTI4_IRQHandler

EXTI9_5_IRQHandler

EXTI15_10_IRQHandler

#注意:0-4有专门的函数名,5-9和10-15都是上面共有的函数名。

(6)编写外部中断控制程序:

使能IO口时钟

 设置IO口与中断线的映射关系

配置中断分组,使能中断

 (IRQ是中断请求通道)

EXTI初始化,选择触发方式

 

编写EXTI中断服务函数

 

3、定时器中断实验

(1)定时器介绍:

STM32F1的定时器非常多,由2个基本定时器(TIM6、TIM7)4个通用定时器(TIM2-TIM5)2个高级定时器(TIM1、TIM8)组成。基本定时器的功能最为简单,类似于51单片机内定时器。通用定时器是在基本定时器的基础上扩展而来,增加了输入捕获与输出比较等功能。高级定时器又是在通用定时器基础上扩展而来,增加了可编程死区互补输出、重复计数器、带刹车(断路)功能,这些功能主要针对工业电机控制方面。

通用定时器简介

STM32F1的通用定时器包含一个 16 位自动重载计数器(CNT),该计数器由可编程预分频器(PSC)驱动。STM32F1的通用定时器可用于多种用途,包括测量输入信号的脉冲宽度(输入捕获)或者生成输出波形(输出比较和PWM)等。 使用定时器预分频器和 RCC 时钟控制器预分频器,脉冲长度和波形周期可以在几个微秒到几个毫秒间调整。STM32F1 的每个通用定时器都是完全独立的,没有互相共享的任何资源。

STM32F1的通用定时器TIMx (TIM2-TIM5 )具有如下功能

(1)16 位向上、向下、向上/向下自动装载计数器(TIMx_CNT)。

(2)16 位可编程(可以实时修改)预分频器(TIMx_PSC),计数器时钟频率的分频系数为 1~65535之间的任意数值。

(3)4个独立通道(TIMx_CH1-4),这些通道可以用来作为

A. 输入捕获

B. 输出比较

C. PWM生成(边缘或中间对齐模式)

D. 单脉冲模式输出

(4)可使用外部信号(TIMx_ETR)控制定时器,且可实现多个定时器互连(可以使用1个定时器控制另外一个定时器)的同步电路。

(5)发生如下事件时产生中断/DMA请求:

A. 更新:计数器向上或向下溢出,计数器初始化(通过软件或者内部、外部触发)

B. 出发时间(计数器启动、停止、初始化或者内部、外部触发计数)

C. 输入捕获

D. 输出比较

(6)支持针对定位的增量(正交)编码器和霍尔传感器电路

(7)触发输入作为外部时钟或者按周期的电流管理

通用定时器结构框图

(2)通用定时器配置步骤:

(1)使能定时器时钟

 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);//使能TIM4时钟

(2)初始化定时器参数,包括自动重装载值,分频系数,计数方式等voidTIM_TimeBaseInit(TIM_TypeDef*TIMx,TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);

typedef struct

{

  uint16_t TIM_Prescaler;         //定时器预分频器

  uint16_t TIM_CounterMode;       //计数模式

  uint32_t TIM_Period;            //定时器周期

  uint16_t TIM_ClockDivision;     //时钟分频

  uint8_t TIM_RepetitionCounter;  //重复计数器

} TIM_TimeBaseInitTypeDef;

了解结构体成员功能后,就可以进行配置,例如:

  TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;

  TIM_TimeBaseInitStructure.TIM_Period=1000;   //自动装载值

  TIM_TimeBaseInitStructure.TIM_Prescaler=35999; //分频系数(自动+1)

  TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;

  TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //设置向上计数模式

  TIM_TimeBaseInit(TIM4,&TIM_TimeBaseInitStructure);

重复计时器是高级定时器配置的参数。

定时器定时时间计算公式如下:

Tout = ((per)*(psc+1))/ Tclk

1000*36000/72M = 500ms

自动重装值*(分频系数+1)/时钟源

(3)设置定时器中断类型,并使能

void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState)

(4)设置定时器中断优先级,使能定时器中断通道

NVIC初始化库函数是NVIC_Init()

(5)开启定时器

void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);

(6)编写定时器中断服务函数

TIM4_IRQHandler

ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);

if(TIM_GetITStatus(TIM4,TIM_IT_Update))

  {

  ...//执行TIM4更新中断内控制

  }

void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);

(3)编写定时器中断控制程序:

下面实现:通过TIM4的更新中断控制D2指示灯间隔500ms状态取反,主函数控制D1指示灯不断闪烁。程序框架如下:

(1)初始化TIM4,并使能更新中断等;

(2)编写TIM4中断服务函数;

(3)编写主函数。

4、PWM呼吸灯实验

(1)PWM简介:

PWM是 Pulse Width Modulation 的缩写,中文意思就是脉冲宽度调制,简称脉宽调制。它是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,其控制简单、灵活和动态响应好等优点而成为电力电子技术最广泛应用的控制方式,其应用领域包括测量,通信,功率控制与变换,电动机控制、伺服控制、调光、开关电源,甚至某些音频放大器,因此学习PWM具有十分重要的现实意义。  其实我们也可以这样理解,PWM是一种对模拟信号电平进行数字编码的方法。通过高分辨率计数器的使用,方波的占空比被调制用来对一个具体模拟信号的电平进行编码。PWM 信号仍然是数字的,因为在给定的任何时刻,满幅值的直流供电要么完全有(ON),要么完全无(OFF)。电压或电流源是以一种通(ON)或断(OFF)的重复脉冲序列被加到模拟负载上去的。通的时候即是直流供电被加到负载上的时候,断的时候即是供电被断开的时候。只要带宽足够,任何模拟值都可以使用 PWM 进行编码

可以将上图中模拟正弦波形曲面多边形面积和PWM等效正弦波脉冲面积相等。这样就不仅能实现高电平3.3V低电平0V,也可以实现0-3.3V。

(2)STM32F1 PWM介绍

STM32F1除了基本定时器TIM6和TIM7,其他定时器都可以产生PWM输出(通用和高级定时器)。其中高级定时器 TIM1 和 TIM8 可以同时产生多达 7 路的 PWM 输出。而通用定时器也能同时产生多达 4路的 PWM 输出,这些在定时器中断章节中已经介绍过。  PWM的输出其实就是对外输出脉宽可调(即占空比调节)的方波信号,信号频率是由自动重装寄存器 ARR 的值决定,占空比由比较寄存器 CCR 的值决定。

 PWM输出比较模式总共有8种,具体由寄存器 CCMRx 的位 OCxM[2:0]配置。我们这里只讲解最常用的两种PWM输出模式:PWM1和PWM2,其他几种模式可以参考《STM32F10x中文参考手册》13、14、15定时器章节。

PWM1和PWM2这两种模式用法差不多,区别之处就是输出电平的极性不同。(下图中有效电平不一定是高电平1,可以人为设置)

 PWM模式根据计数器CNT计数方式,可分为边沿对齐模式和中心对齐模式。

#具体模式说明见23.PWM呼吸灯实验PPT和讲解视频。

(3)PWM输出配置步骤

(1)使能定时器及端口时钟,并设置引脚复用器映射RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);

 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);

GPIO_PinRemapConfig(GPIO_FullRemap_TIM3,ENABLE);

可选的参数在 stm32f10x_gpio.h 都已经列出来非常详细GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;//复用推挽输出

(2)初始化定时器参数,包含自动重装载值,分频系数,计数方式等voidTIM_TimeBaseInit(TIM_TypeDef*TIMx,TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);

(3)初始化PWM输出参数,包含PWM模式,输出极性,使能等

void TIM_OCxInit(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);

typedef struct

{

  uint16_t TIM_OCMode;        //比较输出模式

  uint16_t TIM_OutputState;   //比较输出使能

  uint16_t TIM_OutputNState;  //比较互补输出使能

  uint32_t TIM_Pulse;         //脉冲宽度

  uint16_t TIM_OCPolarity;    //输出极性

  uint16_t TIM_OCNPolarity;   //互补比较输出极性

  uint16_t TIM_OCIdleState;   //空闲状态下比较输出状态

  uint16_t TIM_OCNIdleState;  //空闲状态下比较输出状态

} TIM_OCInitTypeDef;           

  如果我们要配置TIM3的CH1为PWM1模式,输出极性为低电平,并且使能PWM输出,可以如下配置:

TIM_OCInitTypeDef TIM_OCInitStructure;

TIM_OCInitStructure.TIM_OCMode=TIM_OCMode_PWM1;

TIM_OCInitStructure.TIM_OCPolarity=TIM_OCPolarity_Low;

TIM_OCInitStructure.TIM_OutputState=TIM_OutputState_Enable;

        TIM_OC1Init(TIM3,&TIM_OCInitStructure); //输出比较通道1初始化             

(4)开启定时器

void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);TIM_Cmd(TIM3,ENABLE); //开启定时器

(5)修改TIMx_CCRx的值控制占空比

void TIM_SetCompare1(TIM_TypeDef* TIMx, uint32_t Compare1);

(6)使能TIMx在CCRx上的预装载寄存器

使能输出比较预装载库函数是:

void TIM_OCxPreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);

第一个参数用于选择定时器,第二个参数用于选择使能还是失能输出比较预装载寄存器,可选择为TIM_OCPreload_Enable、TIM_OCPreload_Disable。

(7)使能TIMx在ARR上的预装载寄存器允许位

使能 TIMx 在 ARR 上的预装载寄存器允许位库函数是

void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);

第一个参数用于选择定时器,第二个参数用于选择使能还是失能。

高级定时器要输出PWM波形,必须要设置一个MOE位(TIMx_BDTR的第15位),以使能主输出,

(4)编写PWM输出控制程序

下面要实现的功能是:通过TIM3的CH2输出一个PWM信号,控制DS0指示灯由暗变亮,再由亮变暗,类似于人的呼吸。程序框架如下:

(1)初始化PB5管脚为PWM输出功能

(2)PWM输出控制程序

通过库函数TIM_SetCompare2()来改变CCR的值。

二、STM32 ADC转换实验代码阅读

1、STM32F1ADC结构框图介绍

STM32F1 ADC介绍:

ADC(analog to digital converter)即模数转换器,它可以将模拟信号转换为数字信号。按照其转换原理主要分为逐次逼近型、双积分型、电压频率转换型三种。STM32F1ADC就是逐次逼近型的模拟数字转换器。

STM32F103 系列一般都有 3 个 ADC,这些 ADC 可以独立使用,也可以使用双重(提高采样率)。STM32F1 ADC 是 12 位逐次逼近型的模拟数字转换器。它具有多达 18个复用通道,可测量来自16 个外部源、2 个内部源信号。 这些通道的 A/D 转换可以单次、连续、扫描或间断模式执行。ADC 的结果可以左对齐或右对齐方式存储在 16 位数据寄存器中。ADC具有模拟看门狗特性,允许应用程序检测输入电压是否超出用户定义的阀值上限或者下限。

           

结构框图:

默认:V_REF- 和 V_SSA 接GND,V_REF+ 和 V_DDA 接3.3V,这样采样范围就是0-3.3

 (1)标号1:电压输入引脚

  ADC输入电压范围为: VREF- ≤ VIN ≤ VREF+。由 VREF-、 VREF+ 、 VDDA 、 VSSA这四个外部引脚决定。通常我们把 VSSA和 VREF-接地,把 VREF+和 VDDA 接 3.3V,因此ADC的输入电压范围为:0~3.3V。我们使用的开发板ADC输入电压范围为0~3.3V。

(2)标号2:输入通道

  STM32 的 ADC的输入通道多达 18 个,其中外部的 16 个通道就是框图中的 ADCx_IN0、ADCx_IN1...ADCx_IN5(x=1/2/3,表示ADC数),通过这16个外部通道可以采集模拟信号。这 16 个通道对应着不同的 IO 口, 具体是哪一个 IO 口可以从数据手册查询到,也可以从图查看,同样我们在开发板芯片原理图内也给大家标注了。其中 ADC1 还有2个内部通道:ADC1 的通道16连接到了芯片内部的温度传感器,通道17连接到了内部参考电压 VREFINT。ADC2 和ADC3的通道 16、 17全部连接到了内部的 VSS。

 (3)标号3:通道转换顺序

  外部的 16 个通道在转换的时候可分为2组通道:规则通道组和注入通道组,其中规则通道组最多有16路,注入通道组最多有 4 路。

规则通道组:从名字来理解,规则通道就是一种规规矩矩的通道,类似于正常执行的程序,通常我们使用的都是这个通道。

注入通道组:从名字来理解,注入即为插入,是一种不安分的通道,类似于中断。当程序正常往下执行时,中断可以打断程序的执行。同样如果在规则通道转换过程中,有注入通道插入,那么就要先转换完注入通道,等注入通道转换完成后再回到规则通道的转换流程。 

每个组包含一个转换序列,该序列可按任意顺序在任意通道上完成。例如,可按以下顺序对序列进行转换: ADC_IN3、ADC_IN8、 ADC_IN2、 ADC_IN2、 ADC_IN0、 ADC_IN2、 ADC_IN2、 ADC_IN15。

(4)标号4:触发源

  选择好输入通道,设置好转换顺序,接下来就可以开始转换。要开启ADC转换,可以直接设置ADC 控制寄存器ADC_CR2 的 ADON位为1,即使能ADC。当然ADC还支持外部事件触发转换,触发源有很多,具体选择哪一种触发源,由 ADC 控制寄存器2:ADC_CR2 的 EXTSEL[2:0]和 JEXTSEL[2:0]位来控制。EXTSEL[2:0]用于选择规则通道的触发源,JEXTSEL[2:0]用于选择注入通道的触发源。选定好触发源之后,触发源是否要激活,则由 ADC 控制寄存器ADC_CR2 的 EXTTRIG 和 JEXTTRIG 这两位来激活。

如果使能了外部触发事件,我们还可以通过设置 ADC 控制寄存器 2:ADC_CR2 的EXTEN[1:0]和 JEXTEN[1:0]来控制触发极性,可以有 4 种状态,分别是:禁止触发检测、上升沿检测、下降沿检测以及上升沿和下降沿均检测。

(5)标号5:ADC时钟

  ADC 输入时钟 ADC_CLK 由 APB2经过分频产生,最大值是14MHz,分频因子由 RCC 时钟配置寄存器 RCC_CFGR 的位 15:14 ADCPRE[1:0]设置,可以是 2/4/6/8 分频,注意这里没有 1 分频。我们知道APB2总线时钟为72M,而ADC最大工作频率为14M,所以一般设置分频因子为6,这样ADC的输入时钟为12M。  ADC要完成对输入电压的采样需要若干个ADC_CLK周期,采样的周期数可通过ADC 采样时间寄存器 ADC_SMPR1 和 ADC_SMPR2 中的 SMP[2:0]位设置, ADC_SMPR2控制的是通道 0~9, ADC_SMPR1 控制的是通道 10~17。每个通道可以分别用不同的时间采样。其中采样周期最小是1.5个,即如果我们要达到最快的采样,那么应该设置采样周期为1.5个周期,这里说的周期就是 1/ADC_CLK。

ADC 的总转换时间跟ADC 的输入时钟和采样时间有关,其公式如下

Tconv = 采样时间 + 12.5个周期

其中Tconv为ADC总转换时间,当ADC_CLK=14Mhz的时候,并设置1.5个周期的采样时间,则Tcovn=1.5+12.5=14个周期=1us。

(6)标号6:数据寄存器

  ADC 转换后的数据根据转换组的不同,规则组的数据放在ADC_DR 寄存器内,注入组的数据放在 JDRx内。 

因为STM32F1的ADC是12位转换精度,而数据寄存器是16位,所以ADC在存放数据的时候就有左对齐和右对齐区分。如果是左对齐,AD转换完成数据存放在 ADC_DR 寄存器的[4:15]位内;如果是右对齐,则存放在 ADC_DR 寄存器的[0:11]位内。具体选择何种存放方式,需通过ADC_CR2 的 11 位 ALIGN 设置

(7)标号7:中断

  当发生如下事件且使能相应中断标志位时,ADC能产生中断

1.转换结束(规则转换)与注入转换结束

2.模拟看门狗事件

3.DMA请求

我们知道STM32F1 ADC转换模式有单次转换与连续转换区分  在单次转换模式下,ADC 执行一次转换。可以通过 ADC_CR2 寄存器的SWSTART 位(只适用于规则通道)启动,也可以通过外部触发启动(适用于规则通道和注入通道),这时 CONT 位为 0。以规则通道为例,一旦所选择的通道转换完成,转换结果将被存在 ADC_DR 寄存器中,EOC(转换结束)标志将被置位,如果设置了 EOCIE,则会产生中断。然后 ADC 将停止,直到下次启动

在连续转换模式下,ADC 结束一个转换后立即启动一个新的转换。CONT 位为 1 时,可通过外部触发或将 ADC_CR2 寄存器中的 SWSTRT 位置 1 来启动此模式(仅适用于规则通道)。需要注意的是:此模式无法连续转换注入通道。连续模式下唯一的例外情况是,注入通道配置为在规则通道之后自动转换(使用 JAUTO 位)。

2、AD模数转换实验

ADC配置步骤:

接下来我们介绍下如何使用库函数对ADC进行配置。这个也是在编写程序中必须要了解的。具体步骤如下:(ADC相关库函数在stm32f10x_adc.c和stm32f10x_adc.h文件中)

(1)使能端口时钟和ADC时钟,设置引脚模式为模拟输入RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);

GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN; //模拟输入模式

(2)设置ADC的分频因子

RCC_ADCCLKConfig(RCC_PCLK2_Div6); 

(3)初始化ADC参数,包括ADC工作模式、规则序列等

void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct);

typedef struct

{

  uint32_t ADC_Mode; // ADC 工作模式选择

  FunctionalState ADC_ScanConvMode; /* ADC 扫描(多通道)或者单次(单通道)模式选择 */

  FunctionalState ADC_ContinuousConvMode; // ADC 单次转换或者连续转换选择

  uint32_t ADC_ExternalTrigConv; // ADC 转换触发信号选择

  uint32_t ADC_DataAlign; // ADC 数据寄存器对齐格式

  uint8_t ADC_NbrOfChannel; // ADC 采集通道数

} ADC_InitTypeDef;

ADC_InitTypeDef       ADC_InitStructure;

ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;

ADC_InitStructure.ADC_ScanConvMode = DISABLE;//非扫描模式 

ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//关闭连续转换

ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//禁止触发检测,使用软件触发

ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//右对齐 

ADC_InitStructure.ADC_NbrOfChannel = 1;//1个转换在规则序列中 也就是只转换规则序列1

ADC_Init(ADC1, &ADC_InitStructure);//ADC初始化

(4)使能ADC并校准

void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState); 

ADC_Cmd(ADC1, ENABLE);//开启AD转换器

执行复位校准的方法是:

ADC_ResetCalibration(ADC1);

执行 ADC 校准的方法是

ADC_StartCalibration(ADC1); //开始指定 ADC1 的校准状态

while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束

while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束

(5)读取ADC转换值

设置规则序列通道以及采样周期的库函数是:

void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t

ADC_Channel,uint8_t Rank, uint8_t ADC_SampleTime);

ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5 )

设置好规则序列通道及采样周期,接下来就要开启转换,由于我们采用的是软件触发,库函数

void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

开启转换之后,就可以获取ADC 转换结果数据,调用的库函数是

uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);

获取 AD 转换的状态信息的库函数是

FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);

例如我们要判断 ADC1 的转换是否结束,方法是

while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束

硬件电路:

本实验使用到硬件资源如下

(1)D1指示灯

(2)串口1

(3)ADC1_IN1

(4)电位器

D1指示灯、串口1电路在前面章节都介绍过,这里就不多说,至于ADC_IN1它属于STM32F1芯片内部的资源,对应芯片的PA1引脚。

3、USART串口通信

通信基本概念:

通信按照数据传送方式可分为串行通信和并行通信。按照通信的数据同步方式,可分为异同通信和同步通信。按照数据的传输方向又可分为单工、半双工和全双工通信。下面我们就来简单介绍这几种通信方式。

(1)串行通信:

串行通信是指使用一条数据线,将数据一位一位地依次传输,每一位数据占据一个固定的时间长度。其只需要少数几条线就可以在系统间交换信息,特别适用于计算机与计算机、计算机与外设之间的远距离通信。

 串行通信的特点:传输线少,长距离传送时成本低,且可以利用电话网等现成的设备,但数据的传送控制比并行通信复杂。

(2)并行通信:

并行通信通常是将数据字节的各位用多条数据线同时进行传送,通常是8位、16位、32位等数据一起传输。

 并行通信的特点:控制简单、传输速度快;由于传输线较多,长距离传送时成本高且接收方的各位同时接收存在困难,抗干扰能力差。

(3)异步通信:

异步通信是指通信的发送与接收设备使用各自的时钟控制数据的发送和接收过程。为使双方的收发协调,要求发送和接收设备的时钟尽可能一致。

异步通信是以字符(构成的帧)为单位进行传输,字符与字符之间的间隙(时间间隔)是任意的,但每个字符中的各位是以固定的时间传送的,即字符之间不一定有“位间隔”的整数倍的关系,但同一字符内的各位之间的距离均为“位间隔”的整数倍。

 

 异步通信的特点:不要求收发双方时钟的严格一致,实现容易,设备开销较小,但每个字符要附加2~3位用于起止位,各帧之间还有间隔,因此传输效率不高。

(4)同步通信:

同步通信时要建立发送方时钟对接收方时钟的直接控制,使双方达到完全同步。此时,传输数据的位之间的距离均为“位间隔”的整数倍,同时传送的字符间不留间隙,即保持位同步关系,也保持字符同步关系。发送方对接收方的同步可以通过两种方法实现。

(5)单工通信:

单工是指数据传输仅能沿一个方向,不能实现反向传输。

(6)半双工通信:

半双工是指数据传输可以沿两个方向,但需要分时进行。

(7)全双工通信:

全双工是指数据可以同时进行双向传输

通信速率:

衡量通信性能的一个非常重要的参数就是通信速率,通常以比特率(Bitrate)来表示。比特率是每秒钟传输二进制代码的位数,单位是:位/秒(bps)。如每秒钟传送240个字符,而每个字符格式包含10位(1个起始位、1个停止位、8个数据位),这时的比特率为:

10位×240个/秒 = 2400 bps

USART介绍:

串口通信简介:

串口通信(Serial Communication),是指外设和计算机间,通过数据信号线、地线等,按位进行传输数据的一种通信方式,属于串行通信方式。串口是一种接口标准,它规定了接口的电气标准,没有规定接口插件电缆以及使用的协议。

(1)接口标准

串口通信的接口标准有很多,有RS-232C、RS-232、RS-422A、RS-485等。常用的就是RS-232和RS-485。RS-232其实是RS-232C的改进,原理是一样的。这里我们就以RS-232C接口进行讲解,RS-485在后面章节中会介绍。

RS-232C是EIA(美国电子工业协会)1969年修订RS-232C标准。RS-232C定义了数据终端设备(DTE)与数据通信设备(DCE)之间的物理接口标准。

RS-232C接口规定使用25针连接器,简称DB25,连接器的尺寸及每个插针的排列位置都有明确的定义

 

公头和母头的管脚定义顺序是不一样,这一点需要特别注意。常用管脚

的功能如下:

 RS-232C对逻辑电平也做了规定,如下

在TXD和RXD数据线上:

1.逻辑1为-3~-15V的电压

2.逻辑0为3~15V的电压

在RTS、CTS、DSR、DTR和DCD等控制线上:

1.信号有效(ON状态)为3~15V的电压

2.信号无效(OFF状态)为-3~-15V的电压

由此可见,RS232C是用正负电压来表示逻辑状态,与晶体管-晶体管逻辑集成电路(TTL)以高低电平表示逻辑状态的规定正好相反(在硬件电路连接上交叉连接)

串口通信中还需要注意的是,串口数据收发线要交叉连接,计算机的TXD要对应单片机的RXD,计算机的RXD要对应单片机的TXD,并且共GND,如下图:

(2)通信协议

 RS232的通信协议比较简单,通常遵循96-N-8-1格式

96:表示的是通信波特率为9600。串口通信中通常使用的是异步串口通信,即没有时钟线,所以两个设备要通信,必须要保持一致的波特率,当然,波特率常用值还有4800、15200 等。

N:表示的是无校验位,由于串口通信相对更容易受到外部干扰导致传输数据出现偏差,可以在传输过程加上校验位来解决这个问题。校验方法有奇校验(odd)、偶校验(even)、0 校验(space)、1 校验(mark)以及无校验(noparity)。具体的介绍,大家可以百度下串口通信了解。

8:表示的是数据位数为8 位,其数据格式在前面介绍异步通信中已讲过。当然数据位数还可以为5、6、7 位长度。

1:表示的是1 位停止位,串口通讯的一个数据包从起始信号开始,直到停止信号结束。数据包的起始信号由一个逻辑0 的数据位表示,而数据包的停止信号可由0.5、1、1.5 或2 个逻辑1 的数据位表示,只要双方约定一致即可。

USART简介:

USART即通用同步异步收发器,它能够灵活地与外部设备进行全双工数据交换,满足外部设备对工业标准 NRZ 异步串行数据格式的要求。UART即通用异步收发器,它是在USART基础上裁剪掉了同步通信功能,同步和异步主要看其时钟是否需要对外提供,这个前面也介绍了。我们开发板上使用的STM32F103ZET6芯片含有3个USART,2个UART外设。它们都具有串口通信功能,USART它支持同步单向通信和半双工单线通信;还支持 LIN(域互连网络)、智能卡协议与 IrDA(红外线数据协会) SIR ENDEC 规范,以及调制解调器操作 (CTS/RTS)。而且,它还支持多处理器通信和DMA功能,使用 DMA 可实现高速数据通信。USART 通过小数波特率发生器提供了多种波特率。

USART在STM32中应用最多的是printf输出调试信息,当我们需要了解程序内的一些变量数据信息时,可以通过printf输出函数将这些信息打印到串口助手上显示,这样一来就给我们调试程序带来了极大的方便。

USART结构框图:

我们把上图分为几个模块进行介绍:

(1)标号1:功能引脚
TX:发送数据输出引脚。
RX:接收数据输入引脚。
SW_RX:数据接收引脚,只用于单线和智能卡模式,属于内部引脚,没有具体外部引脚。
nRTS:请求以发送(Request To Send),n 表示低电平有效。如果使能RTS 流控制,当USART 接收器准备好接收新数据时就会将nRTS 变成低电平;当接收寄存器已满时,nRTS 将被设置为高电平。该引脚只适用于硬件流控制。
nCTS:清除以发送(Clear To Send),n 表示低电平有效。如果使能CTS 流控制,发送器在发送下一帧数据之前会检测nCTS 引脚,如果为低电平,表示可以发送数据,如果为高电平则在发送完当前数据帧之后停止发送。该引脚只适用于硬件流控制。
SCLK:发送器时钟输出引脚。这个引脚仅适用于同步模式

前面我们说了,STM32F103ZET6 芯片具有5 个串口外设,其对应的管脚可在芯片数据手册上查找到,也可以直接查看我们开发板原理图,我们已经将芯片所有的IO 口功能都标注在管脚上了。USART1 挂接在APB2 总线上,其他的挂接在APB1 总线,由于UART4 和UART5 只有异步传输功能,所以没有SCLK、nCTS 和nRTS脚,如下:

(2)标号2:数据寄存器
USART 数据寄存器(USART_DR)只有低9 位有效,并且第9 位数据是否有效要取决于USART 控制寄存器1(USART_CR1)的M 位设置,当M 位为0 时表示8 位数据字长,当M 位为1 表示9 位数据字长,我们一般使用8 位数据字长。USART_DR 包含了已发送的数据或者接收到的数据。
USART_DR 实际是包含了两个寄存器,一个专门用于发送的可写TDR,一个专门用于接收的可读RDR。当进行发送操作时,往USART_DR 写入数据会自动存储在TDR 内;当进行读取操作时,向USART_DR 读取数据会自动提取RDR 数据。
TDR 和RDR 都是介于系统总线和移位寄存器之间。串行通信是一个位一个位传输的,发送时把TDR 内容转移到发送移位寄存器,然后把移位寄存器数据每一位发送出去,接收时把接收到的每一位顺序保存在接收移位寄存器内然后才转移到RDR。
USART 支持DMA 传输,可以实现高速数据传输,具体DMA 使用在后面章节
中会介绍。


(3)标号3:控制器
USART 有专门控制发送的发送器、控制接收的接收器,还有唤醒单元、中断控制等等。使用USART 之前需要向USART_CR1 寄存器的UE 位置1 使能USART。发送或者接收数据字长可选8 位或9 位,由USART_CR1 的M 位控制。
①发送器
发送器可发送8 位或9 位的数据,具体取决于M 位的状态。发送使能位 (TE) 置1 时,发送移位寄存器中的数据在TX 引脚输出,如果是同步通信模式,相应的时钟脉冲在SCLK 引脚输出。

②接收器
如果将USART_CR1 寄存器的RE 位置1,使能USART 接收,使得接收器在RX 线开始搜索起始位。在确定到起始位后就根据RX 线电平状态把数据存放在接收移位寄存器内。接收完成后就把接收移位寄存器数据移到RDR 内,并把USART_SR 寄存器的RXNE 位置1,同时如果USART_CR2 寄存器的RXNEIE 置1的话可以产生中断。

③中断控制
USART 有多个中断请求事件,如下:

USART 中断事件被连接到相同的中断向量,如下:

(4)标号4:波特率生成

波特率的概念在前面介绍比特率的时候已经提过,常用的串口通信中都把波特率当作比特率。波特率越大,传输速度就越快。
接收器和发送器( Rx 和Tx)的波特率均设置为相同值。波特率计算公式如下:

其中,fCK 为USART 时钟频率,USARTDIV 是一个存放在波特率寄存器(USART_BRR)的一个无符号定点数。其中DIV_Mantissa[11:0]位定义USARTDIV的整数部分,DIV_Fraction[3:0]位定义USARTDIV 的小数部分。
串口通信中常用的波特率为4800、9600、115200 等。 

USART串口通信配置:

在上面的介绍中,可能有的朋友很不理解,不过没有关系,下面我们讲解如何使用库函数对USART 进行配置。这个也是在编写程序中必须要了解的。具体步骤如下:(USART 相关库函数在stm32f10x_usart.c 和stm32f10x_usart.h 文件中)

(1)使能串口时钟及GPIO端口时钟

前面说过STM32F103ZET6 芯片具有5 个串口,对应不同的引脚,串口1 挂接在APB2 总线上,串口2-串口5 挂接在APB1 总线上,根据自己所用串口使能总线时钟和端口时钟。例如使用USART1,其挂接在APB2 总线上,并且USART1 对应STM32F103ZET6 芯片管脚的PA9 和PA10,因此使能时钟函数如下:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //使能GPIOA时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//使能USART1时钟

(2)GPIO端口模式设置,设置串口对应的引脚为复用功能

因为使用引脚的串口功能,所以在配置GPIO 时要将设置为复用功能,这里把串口的Tx 引脚配置为复用推挽输出, Rx 引脚为浮空输入,数据完全由外部输入决定。如下:

  

(3)初始化串口参数,包含波特率、字长、奇偶校验等参数

要使用串口功能,必须对串口通信相关参数初始化,其库函数如下:

 想必不用说,大家也知道第一个参数是什么意思,它是用来选择串口。第二个参数是一个结构体指针变量,结构体类型是USART_InitTypeDef,其内包含了串口初始化的成员变量。下面我们就来看下这个结构体:

 下面就来简单介绍下每个成员变量的功能:

USART_BaudRate:波特率设置。常用的波特率为4800、9600、115200 等。标准库函数会根据设定值计算得到USARTDIV 值, 并设置USART_BRR 寄存器值。

USART_WordLength:数据帧字长。可以选择为8 位或者9 位,通过USART_CR1寄存器的M 位的值决定。如果没有使能奇偶校验控制,一般使用8 数据位;如果使能了奇偶校验则一般设置为9 数据位。
USART_StopBits:停止位设置。可选0.5 个、1 个、1.5 个和2 个停止位,它设定USART_CR2 寄存器的STOP[1:0]位的值,一般我们选择1 个停止位。
USART_Parity:奇偶校验控制选择。可选USART_Parity_No( 无校验) 、USART_Parity_Even( 偶校验) 以及USART_Parity_Odd( 奇校验) ,它设定USART_CR1 寄存器的PCE 位和PS 位的值。
USART_Mode:USART 模式选择。可以为USART_Mode_Rx 和USART_Mode_Tx,允许使用逻辑或运算选择两个,它设定USART_CR1 寄存器的RE 位和TE 位。USART_HardwareFlowControl:硬件流控制选择。只有在硬件流控制模式才有效,可以选择无硬件流USART_HardwareFlowControl_None、RTS 控制USART_HardwareFlowControl_RTS、CTS 控制USART_HardwareFlowControl_CTS、RTS 和CTS 控制USART_HardwareFlowControl_RTS_CTS。

了解结构体成员功能后,就可以进行配置,例如我们配置USART1,如下:

(4)使能串口

配置好串口后,我们还需要使能它,使能串口库函数如下:

例如我们要使能USART1,如下:

(5)设置串口中断类型并使能

对串口中断类型和使能设置的函数如下:

 第一个参数用来选择串口,第二个参数用来选择串口中断类型,第三个参数用来使能或者失能对应中断。由于串口中断类型比较多,所以使用哪种中断,我们就需要对它进行配置。比如在接收到数据的时候(RXNE 读数据寄存器非空),我们要产生中断,那么我们开启中断的方法是:

 又比如我们发送完数据时,要产生中断,可以配置如下:

 对应的串口中断类型可在stm32f10x_usart.h 中查找到,如下:

  

(6)设置串口中断优先级,使能串口中断通道

在上一步我们已经使能了串口的接收中断,只要使用到中断,就必需对NVIC
初始化,NVIC 初始化库函数是NVIC_Init(),这个在前面讲解STM32 中断时就已
经介绍过,不清楚的可以回过头看下。

(7)编写串口中断服务函数

最后我们还需要编写一个串口中断服务函数,通过中断函数处理串口产生的相关中断。串口中断服务函数名在STM32F1 启动文件内就有,USART1 中断函数名如下:

 因为串口的中断类型有很多,所以进入中断后,我们需要在中断服务函数开头处通过状态寄存器的值判断此次中断是哪种类型,然后做出相应的控制。库函数中用来读取串口中断状态标志位的函数如下:

 此函数功能是判断USARTx 的中断类型USART_IT 是否产生中断,例如我们要判断USART1 的接收中断是否产生,可以调用此函数:

 如果产生接收中断,那么调用USART_GetITStatus 函数后返回值为1,就会进入到if 函数内执行中断控制功能程序。否则就不会进入中断处理程序。

在编写串口中断服务函数时,最后通常会调用一个清除中断标志位的函数,如下:

 第二个参数为状态标志选项,可选参数可在stm32f10x_usart.h 中查找到,如下:

 比如判断串口进入接收中断后,我们就会把串口接收寄存器内数据读取出来,然后再通过串口发送至上位机,等待发送完成后我们就会清除发送完成标志位USART_FLAG_TC。代码如下:

 串口接收函数是:

 串口发送函数是:

 库函数中还有一个函数用来读取串口状态标志位:

 USART_GetITStatus 与USART_GetFlagStatus 功能类似,区别就是USART_GetITStatus 函数会先判断是否使能串口中断,使能后才读取状态标志,而USART_GetFlagStatus 函数直接读取状态标志。

将以上几步全部配置好后,我们就可以正常使用串口中断了。

4、printf重定向

printf重定向基本概念

printf重定向简介:

我们知道C 语言中printf 函数默认输出设备是显示器,如果要实现在串口或者LCD 上显示,必须重定义标准库函数里调用的与输出设备相关的函数。比如使用printf 输出到串口,需要将fputc 里面的输出指向串口,这一过程就叫重定向。
那么如何让STM32 使用printf 函数呢?很简单,只需要将fputc 里面的输出指向STM32 串口即可,fputc 函数有固定的格式,我们只需要在函数内操作STM32 串口即可,代码如下:

 如果要让其他的串口也使用printf 函数,只需要修改下串口号即可。

printf函数格式:

printf 函数调用格式如下:

 其中格式化字符串包括两部分内容: 一部分是正常字符, 这些字符将按原样输出;另一部分是格式化规定字符, 以"%"开始, 后跟一个或几个规定字符,用来确定输出内容格式。

参量表是需要输出的一系列参数, 其个数必须与格式化字符串所说明的输出参数个数一样多, 各参数之间用","分开, 且顺序一一对应, 否则将会出现意想不到的错误。

记得包含头文件<stdio.h>!!!

常用格式化规定字符如下:

 在KEIL 中使用printf 一定要勾选“微库”选项,否则不会输出。配置如下:

 在STM32 程序开发中printf 应用是非常广的,当我们需要查看某些变量数
值或者其他信息等,都可以通过printf 打印到串口调试助手上查看。

三、FSMC-TFTLCD显示实验代码阅读

1、TFTLCD和FSMC介绍

TFTLCD简介

FSMC简介

#以上图片均来自普中STM32带你进入ARM世界系列视频

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
STM32F1是STMicroelectronics公司推出的一款32位微控制器系列。虽然主要用于工业控制和嵌入式系统发,但也可以用于游戏发。下面是一个示例的300字文回答,介绍如何编写STM32F1游戏代码。 要编写STM32F1游戏代码,首先需要了解STM32F1的硬件和编程环境。硬件方面,我们需要一块STM32F1发板,例如STM32F103C8T6。这个发板上有一个ARM Cortex-M3处理器,以及一系列引脚和外设,可以用来连接游戏输入和输出设备,如按键、LED灯和液晶显示屏。 在编程环境方面,我们需要准备一款支持ARM Cortex-M处理器的集成发环境(IDE),如Keil MDK或STM32CubeIDE。这些IDE提供了编译器、调试器和工程管理工具,方便我们编写和调试STM32F1游戏代码。 编写游戏代码时,首先需要设置引脚和外设的初始化,以确定输入和输出的方式。例如,我们可以将按键连接到引脚上,并使用GPIO外设来读取按键状态。根据不同的按键状态,我们可以执行不同的游戏逻辑或控制游戏角色移动。 接下来,我们需要编写游戏逻辑和图形显示代码。根据游戏的需求,我们可以使用STM32F1的图形库或源游戏引擎,如LittlevGL,来绘制游戏界面和动画效果。同时,我们可以使用定时器断来控制游戏的帧率和更新速度。 最后,我们需要添加游戏音效。可以使用STM32F1的DAC或PWM模块来生成声音效果,并将其连接到扬声器或耳机上。 总之,编写STM32F1游戏代码需要理解STM32F1的硬件和编程环境,设置引脚和外设的初始化,编写游戏逻辑和图形显示代码,并添加音效。通过充分利用STM32F1的功能和资源,我们可以创造出各种有趣的嵌入式游戏。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值