STM32归档(上)

个人学习笔记,将对知识点进行概述,主要学习请参考B站江科大的视频。或者STM32手册。

GPIO

GPIO简介

•GPIO(General Purpose Input Output)通用输入输出口

•可配置为8种输入输出模式

•引脚电平:0V~3.3V,部分引脚可容忍5V

•输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等

•输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等

GPIO基本结构

所有GPIO都挂载在APB2总线上。PA0~PB15为GPIO外设引脚

寄存器是一段特殊的存储器,内核可以通过APB2总线对寄存器进行读写,以完成输出电平和读取电平的功能,但只有低16位才有对应端口。

驱动器负责增大驱动能力。

GPIO位结构

GPIO模式

推挽输出的高低电平均有较强的驱动能力。

浮空/上拉/下拉输入

在输入模式下,出入驱动器断开。

模拟输入

使用ADC的专用配置。输入和输出驱动器都断开无效,信号从引脚后,直接进入片上外设。

开漏/推挽输出

一个端口可以有多个输入,但只能有一个输出。通过输出驱动器的MOS管,可以控制输出为推挽或开漏模式。

复用开漏/推挽输出

输出控制由片上外设控制,输入时,片上外设和输入驱动器都有效。

外设GPIO配置

示例程序

流水灯

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 延时函数
int main(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);    //开启APB2时钟
    GPIO_InitTypeDef GPIO_InitStructure;                    //为结构体命名
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;        //设为GPIO为推挽输出模式
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;             //所有引脚
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;       //输出速度
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    while (1)
    {
        GPIO_Write(GPIOA, ~0x0001);	//0000 0000 0000 0001   //一次写入16个端口,控制GPIO
        Delay_ms(100);
        GPIO_Write(GPIOA, ~0x0002);	//0000 0000 0000 0010
        Delay_ms(100);
        GPIO_Write(GPIOA, ~0x0004);	//0000 0000 0000 0100
        Delay_ms(100);
        GPIO_Write(GPIOA, ~0x0008);	//0000 0000 0000 1000
        Delay_ms(100);
        GPIO_Write(GPIOA, ~0x0010);	//0000 0000 0001 0000
        Delay_ms(100);
        GPIO_Write(GPIOA, ~0x0020);	//0000 0000 0010 0000
        Delay_ms(100);
        GPIO_Write(GPIOA, ~0x0040);	//0000 0000 0100 0000
        Delay_ms(100);
        GPIO_Write(GPIOA, ~0x0080);	//0000 0000 1000 0000
        Delay_ms(100);
    }
}
 
LED闪烁

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                     //延时函数
int main(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);    //开启时钟
    GPIO_InitTypeDef GPIO_InitStructure;                    //结构体,初始化GPIO
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;       //推挽输出模式
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;              //初始化的引脚为GPIOA的pin0
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;      //运行速度
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    while (1)
    {
        GPIO_ResetBits(GPIOA, GPIO_Pin_0);             //把pin0置为低电平
        Delay_ms(500);
        GPIO_SetBits(GPIOA, GPIO_Pin_0);               //把pin0置为高电平
        Delay_ms(500);
        GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);
        Delay_ms(500);
        GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);
        Delay_ms(500);
        GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)0);
        Delay_ms(500);
        GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)1);
        Delay_ms(500);
    }
}

NVIC

中断系统

中断:     在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行

中断优先级:  当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源

中断嵌套:   当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回

中断执行流程

程序由硬件电路自动跳转到中断程序中

STM32中断

•68个可屏蔽中断通道,包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设

•使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级。

内核中断:

NVIC的基本结构

一个外设可能占用多个中断通道

NVIC优先级分组

•NVIC的中断优先级由优先级寄存器的4位(0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级

•抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队

EXTI外部中断

EXTI简介

•EXTI(Extern Interrupt)外部中断

•EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序

•支持的触发方式:上升沿/下降沿/双边沿/软件触发

•支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断。(PA1和PB1不能同时用)

•通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒

•触发响应方式:中断响应/事件响应

中断响应:让CPU执行中断函数

事件响应:当外部中断检测到引脚变化时,中断信号就不会通向CPU了,而是通到其它外设,用来触发其它外设的操作(ADC,DMA)属于外设之间的联合工作。

EXTI的基本结构

AFIO复用IO口

•AFIO主要用于引脚复用功能的选择和重定义

•在STM32中,AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择

EXTI框图

示例程序

配置过程:

1.    配置RCC,把涉及的外设时钟都打开GPIO,AFIO,(无需:NVIC,EXTI)
2.    配置GPIO,选择端口为输入模式
3.    配置AFIO,选择所需的GPIO链接EXTI。
4.    配置EXTI,选择触发方式和响应方式。(一般为边沿触发,中断响应)
5.    配置NVIC,选择合适的优先级。

最后,通过NVIC,外部中断信号进入CPU。

对射式红外传感器计次

挡光片在对射式红外传感器中间经过时,D0输出电平跳变,触发PB14号IO口的中断。

代码

#include "stm32f10x.h"                  // Device header
uint16_t CountSensor_Count;
void CountSensor_Init(void)
{
//开启相关时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
//利用结构体配置GPIO
    GPIO_InitTypeDef GPIO_InitStructure;                     
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
//选择中断线
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
//利用结构体配置EXTI
    EXTI_InitTypeDef EXTI_InitStructure;
    EXTI_InitStructure.EXTI_Line = EXTI_Line14;
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
    EXTI_Init(&EXTI_InitStructure);
//利用结构体配置NVIC
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);
}
uint16_t CountSensor_Get(void)
{
    return CountSensor_Count;
}
void EXTI15_10_IRQHandler(void)       //中断函数,函数名不可自定义
{
    if (EXTI_GetITStatus(EXTI_Line14) == SET)
    {
        /*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
        if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
        {
            CountSensor_Count ++;
        }
        EXTI_ClearITPendingBit(EXTI_Line14);
    }
}
 
**螺旋编码器计次**

螺旋编码器可用于测量位置,速度或旋转方向,当其旋转轴旋转时,其输出端额可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度与方向。

默认无旋转时,此时上拉为高电平,并输出到A端口,当旋转时内部触电导通,此时因为连接GND,A端口就为低电平。

旋转轴旋转时,此时两触点以相位相差90度的方式交替导通。

示例连接图:

主函数

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Encoder.h"
int16_t Num;
int main(void)
{
    OLED_Init();                     //初始化OLED屏幕
    Encoder_Init();
    OLED_ShowString(1, 1, "Num:");   //OLED显示函数
    while (1)
    {
        Num += Encoder_Get();
        OLED_ShowSignedNum(1, 5, Num, 5);
    }
}

Encoder.c

#include "stm32f10x.h"                  // Device header
int16_t Encoder_Count;
//初始化,配置相关IO口,中断。
void Encoder_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);
    GP_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);
    EXTI_InitTypeDef EXTI_InitStructure;
    EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
    EXTI_Init(&EXTI_InitStructure);
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);
    NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
    NVIC_Init(&NVIC_InitStructure);
}
//放回count的变化值
int16_t Encoder_Get(void)
{
    int16_t Temp;
    Temp = Encoder_Count;
    Encoder_Count = 0;
    return Temp;
}
//中断程序,执行计数
void EXTI0_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line0) == SET)
    {
        /*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
        if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
        {
            if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
            {
                Encoder_Count --;
            }
        }
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}
void EXTI1_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line1) == SET)
    {
        /*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
        if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
        {
            if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
            {
                Encoder_Count ++;
            }
        }
        EXTI_ClearITPendingBit(EXTI_Line1);
    }
}

定时器

TIM简介

•TIM(Timer)定时器

•定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断

•16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时

•不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能

•根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型

应用:利用输出比较功能,可以产生PWM波形,驱动电机。输入捕获功能,可以实现测量方波频率。定时器的编码器接口,也可以更方便的读取正交编码器的输出波形。

定时器类型

高级定时器可用于三相无刷电机。

STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4。

基本定时器

基本定时器一般是连接到内部时钟(72MHz),经过预分频器进行分频。比如:预分频器写0就是不分频或者(1分频),输出频率=输入频率=72M,写1就是二分频,输出=输入/2=36M,以此类推。这个预分频器是16位,最大值可填65535,也就是65536分频。分频后计数器(16位)会不断自增,通过自动重装寄存器(16位)可以控制计数时间。当计数=自动重装值,计数清零,输出中断。

主从模式触发ADC

能让内部的硬件在不受程序的控制下实现自动运行。

通用定时器

高级定时器

定时器结构详解

定时中断基本结构图

预分频器时序

•计数器计数频率:CK_CNT = CK_PSC / (PSC + 1)

计数器时序

•计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1)= CK_PSC / (PSC + 1) / (ARR + 1)

计数器无预装时序

计数器有预装时序

如果引入影子寄存器,当自动加载寄存器的值更改后,当前计数周期结束后,才会更改自动加载寄存器的值。实际上为了同步,让值的变化和事件的更新同步发生,防止运行途中由于更改所造成的错误。

比如:原本计时50秒,闹钟响起。计时到45秒时,改为计时到30秒响起闹钟。那么则是45->60->0->30,闹钟才会想起。引入影子寄存器后,则是45->50,响起闹钟,下一个周期:0->30再响起一次闹钟。

RCC时钟树

STM32用来产生和配置时钟.并吧配置好的时钟发送到各个外设系统。SystemInit就是用来配置时钟树的。

左边是产生时钟电路,右边则是分配电路。两个高速时钟是为系统提供时钟的。一般外部晶振比内部稳定,所以一般使用外部晶振。

SystemInit配置时钟过程:先开启内部8Mhz晶振作为系统时钟,再开启外部时钟(一般为8M),经过倍频且稳定后变为72M,再代替原来内部的8M作为系统时钟。所以,如果外部时钟出了问题,则切换为内部时钟。8M和72M相比,大概慢了10倍。

示例程序

配置过程(内部时钟为例):

1.  RCC开启时钟,此时基准时钟和整个外设的工作时钟都被打开。

2.  选择时基单元的时钟源(内部时钟模式)

3.  配置时基单元
4.  配置输出中断控制,允许更新中断输出到NVIC
5.  配置NVIC,打开中断通道,并分配优先级

定时器中断(内部时钟):

main.c

#include "stm32f10x.h" 
#include "Delay.h" 
#include "OLED.h" 
#include "Timer.h" 
uint16_t Num; 
int main(void)
{
    OLED_Init(); 
    Timer_Init(); 
//
    OLED_ShowString(1, 1, "Num:"); 
    while (1)
    {
        OLED_ShowNum(1, 5, Num, 5); // 在OLED屏幕上显示变量Num的值,位于第1行第5列,总宽度为5个字符
    }
}
//
void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
    {
        Num++;                                      // 每次定时器2的更新中断触发时,增加Num的值
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除定时器2的更新中断标志位,准备下一次触发
    }
}
 
Timer.c

#include "stm32f10x.h" // 包含STM32库的头文件
void Timer_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); 				// 启用TIM2的时钟
    TIM_InternalClockConfig(TIM2); 										// 配置TIM2的时钟源为内部时钟
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;                 //结构体
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; 		// 不分频
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; 	// 向上计数模式
    TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1; 					// ARR自动重装载寄存器的值,决定了计数器溢出的时间间隔
    TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; 				// PSC预分频器的值,决定了计数器的时钟频率
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; 				//重复计数器的值(只有高级定时器才有)
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); 				// 初始化TIM2的时间基准单元
    TIM_ClearFlag(TIM2, TIM_FLAG_Update); 								// 清除TIM2的更新标志位
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); 							// 启用TIM2的更新中断
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 					// 配置NVIC的优先级分组为组2
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;						// 配置中断通道为TIM2
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; 					// 启用中断通道
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; 			// 配置抢占优先级为2
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; 					// 配置子优先级为1
    NVIC_Init(&NVIC_InitStructure); 									// 初始化NVIC
    TIM_Cmd(TIM2, ENABLE); 												// 启用TIM2定时器
}
/*
void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
    {
        // 在这里添加处理TIM2更新中断的代码
        // 可以在此处执行你的特定任务或操作
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);         // 清除TIM2的更新中断标志位
    }
}
*/
 
Timer.h

#ifndef __TIMER_H
#define __TIMER_H
//
        void Timer_Init(void);
//
#endif
 

定时器外部时钟

使用对射式红外传感器来模拟外部时钟。

main.c

#include "stm32f10x.h" 
#include "Delay.h" 
#include "OLED.h" 
#include "Timer.h" 
//
uint16_t Num; 
//
int main(void)
{
    OLED_Init(); 
    Timer_Init();
    OLED_ShowString(1, 1, "Num:"); 
    OLED_ShowString(2, 1, "CNT:"); 
    while (1)
    {
        OLED_ShowNum(1, 5, Num, 5);
        OLED_ShowNum(2, 5, Timer_GetCounter(), 5);
    }
}
//
void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
    {
        Num++;                                      // 每次定时器2的更新中断触发时,增加Num的值
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除定时器2的更新中断标志位,准备下一次触发
    }
}
 
Timer.c

#include "stm32f10x.h" 
//
void Timer_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);       // 启用TIM2的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);     // 启用GPIOA的时钟
//
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;			 // 配置GPIOA的引脚模式为上拉输入模式
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; 				// 配置GPIOA的引脚为引脚0
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 		// 配置GPIOA的引脚速度为50MHz
    GPIO_Init(GPIOA, &GPIO_InitStructure); 					// 初始化GPIOA
// 配置TIM2的外部时钟源模式
    TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F); 
//结构体配置
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;				 // 不分频
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; 		// 向上计数模式
    TIM_TimeBaseInitStructure.TIM_Period = 10-1; 		// 自动重装载寄存器的值,决定了计数器溢出的时间间隔
    TIM_TimeBaseInitStructure.TIM_Prescaler = 1-1; 		// 预分频器的值,决定了计数器的时钟频率
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); 	// 初始化TIM2的时间基准单元
//
    TIM_ClearFlag(TIM2, TIM_FLAG_Update); 		// 清除TIM2的更新标志位
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); 	// 启用TIM2的更新中断
//
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 配置NVIC的优先级分组为组2
//
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; 				// 配置中断通道为TIM2
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; 				// 启用中断通道
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; 		// 配置抢占优先级为2
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; 				// 配置响应优先级为1
    NVIC_Init(&NVIC_InitStructure); 								// 初始化NVIC
//
    TIM_Cmd(TIM2, ENABLE);											 // 使能TIM2定时器
}
//
uint16_t Timer_GetCounter(void)
{
    return TIM_GetCounter(TIM2); // 获取TIM2计数器的当前值
}
/*
void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
    {
        // 在这里添加处理TIM2更新中断的代码
        // 可以在此处执行你的特定任务或操作
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除TIM2的更新中断标志位
    }
}
*/
 
Timer.h

#ifndef __TIMER_H
#define __TIMER_H
//
void Timer_Init(void);
uint16_t Timer_GetCounter(void);
//
#endif
 

PWM

PWM简介

• PWM(Pulse Width Modulation)脉冲宽度调制

• 在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域

• PWM参数:

频率 = 1 / TS 占空比 = TON / TS 分辨率 = 占空比变化步距

简单说,Ts代表一个高低电平变换周期的时间,占空比决定PWM等效出来的模拟电压的大小,分辨率则可以理解为变化的精细,比如:1% → 2%,分辨率为1%。1.1% → 1.2%,分辨率为0.1%

image-20230906144635542

PWM需要用到输出比较,所以先介绍一下stm32的输出比较功能。

输出比较简介

• OC(Output Compare)输出比较

• IC(Input Capture) 输入捕获

• CC(Capture/Compare)输出比较和输入捕获

• 输出比较可以通过比较CNT与CCR寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形

• 每个高级定时器和通用定时器都拥有4个输出比较通道

• 高级定时器的前3个通道额外拥有死区生成和互补输出的功能

输出比较通道(高级)

输出比较通道(通用)

比较CNT(计数器)和CCR(捕获比较寄存器)的值,会改变oc1ref(ref:参考信号)的输出电平。(信号可映射至主模式控制器。)输出经过极性选择(是否翻转电平,0为不翻转),最后输出。

输出比较模式

PWM基本结构

参数计算:

• PWM频率:  Freq = CK_PSC / (PSC + 1) / (ARR + 1)   

• PWM占空比: Duty = CCR / (ARR + 1)

• PWM分辨率: Reso = 1 / (ARR + 1)

示例程序

配置PWM初始化程序的一般步骤

1. 开启TIM外设和GPIO外设的时钟。
2. 配置时基单元。
3. 配置输出比较单元。CCR的值,输出比较模式,极性选择,输出使能
4. 配置GPIO,PWM对应的GPIO,配置为复用推挽输出
5. 运行控制

PWM驱动LED呼吸灯

main.c

#include "stm32f10x.h" 
#include "Delay.h"     
#include "OLED.h"      
#include "PWM.h"       
//
uint8_t i; 
//
int main(void)
{
    OLED_Init(); 
    PWM_Init();  
//
    while (1)
    {
        // 增加PWM占空比从0到100,使亮度逐渐增大
        for (i = 0; i <= 100; i++)
        {
            PWM_SetCompare1(i); // 设置PWM通道1的占空比
            Delay_ms(10);       
        }
        // 降低PWM占空比从100到0,使亮度逐渐减少
        for (i = 0; i <= 100; i++)
        {
            PWM_SetCompare1(100 - i); // 设置PWM通道1的占空比
            Delay_ms(10);             
        }
    }
}
 
PWM.c

#include "stm32f10x.h" 
void PWM_Init(void)
{
// 启用TIM2时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 启用GPIOA时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置TIM2通道1的GPIO引脚
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;     // 配置的GPIO引脚
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置TIM2为内部时钟
    TIM_InternalClockConfig(TIM2);
// 配置TIM2的时基
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;  // ARR
    TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; // PSC
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
// 配置TIM2通道1为PWM输出
    TIM_OCInitTypeDef TIM_OCInitStructure;
    TIM_OCStructInit(&TIM_OCInitStructure);
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 0; // CCR
    TIM_OC1Init(TIM2, &TIM_OCInitStructure);
// 启用TIM2
    TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompare1(uint16_t Compare)
{
    // 设置TIM2通道1的比较值
    TIM_SetCompare1(TIM2, Compare);
}
 
PWM.h

#ifndef __PWM_H
#define __PWM_H
//
void PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);
//
#endif
 

PWM驱动舵机

• 舵机是一种根据输入PWM信号占空比来控制输出角度的装置

• 输入PWM信号要求:周期为20ms,高电平宽度为0.5ms~2.5ms      

   

main.c

#include "stm32f10x.h" 
#include "Delay.h"  //延时
#include "OLED.h"   
#include "Servo.h"
#include "Key.h"    //按键
// 全局变量声明
uint8_t KeyNum;
float Angle;
int main(void)
{
    // 初始化OLED显示屏、舵机和按键
    OLED_Init();
    Servo_Init();
    Key_Init();
    OLED_ShowString(1, 1, "Angle:");
    while (1)
    {
        // 获取按键状态
        KeyNum = Key_GetNum();
        // 如果按键被按下
        if (KeyNum == 1)
        {
            Angle += 30;     // 增加角度值
            if (Angle > 180)
            {
                Angle = 0;   // 限制在0到180度之间
            }
        }
        // 设置舵机的角度
        Servo_SetAngle(Angle);
        // 在OLED上显示当前角度
        OLED_ShowNum(1, 7, Angle, 3);
    }
}
 
Servo.c

#include "stm32f10x.h"                  // Device header
#include "PWM.h"
//初始化,和示例一的PWM初始化一致
void Servo_Init(void)
{
    PWM_Init();
}
//设置PWM脉冲宽度,并且与角度用公式转换,便于写入和理解
void Servo_SetAngle(float Angle)
{
    PWM_SetCompare2(Angle / 180 * 2000 + 500);
}
 

输入捕获

输入捕获简介

• IC(Input Capture)输入捕获

• 输入捕获模式下,当通道输入引脚出现指定电平跳变时,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数

• 每个高级定时器和通用定时器都拥有4个输入捕获通道

• 可配置为PWMI模式,同时测量频率和占空比

• 可配合主从触发模式,实现硬件全自动测量

频率测量

输入捕获通道

滤波器控制滤波参数,边沿检测器可以捕获信号的上升沿,如果将信号映射至从模式控制器,则可以再捕获之后自动完成CNT的清零工作。

主从触发模式

将定时器的内部信号,映射到TRGO引脚,用于触发其它的外设。选择从模式的触发信号源后,可选择从模式控制自身定时器运行。

输入捕获基本结构

只用了一个通道,所以只能测频率

PWMI基本结构

使用2个通道同时捕获一个引脚,CRR1为一整个周期的计数值,CRR2是高电气期间的计数值。则占空比=CCR2/CCR1

示例程序

一般配置步骤

1.使用RCC开启TIM和GPIO的时钟。

2.初始化GPIO,配置为输入模式,一般为上拉/浮空输入

3.配置时基单元,使CNT计数器在内部时钟的驱动下自增运行

4.配置输入捕获单元(包括滤波器,极性选择,直连通道)

5.选择从模式的触发源,选择TI1FP1。

6.选择从模式的触发动作(Reset,自动清零CNT)

7.调用TIM_cmd()启动定时器计数。

输入捕获模式测频率

使用STM32自身IO口,PA0输出信号,PA6接收并检测频率。

main.c

#include "stm32f10x.h" 
// 自定义头文件,相关程序参考文章前置相关
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
//
int main(void)
{
// 初始化
    OLED_Init();
    PWM_Init();
    IC_Init();
    OLED_ShowString(1, 1, "Freq:00000Hz");
// 设置PWM的预分频器和占空比
    PWM_SetPrescaler(720 - 1); // Freq = 72M / (PSC + 1) / 100
    PWM_SetCompare1(50);       // Duty = CCR / 100
    while (1)
    {
// 获取输入捕获模块测得的频率,并在OLED上显示
        OLED_ShowNum(1, 6, IC_GetFreq(), 5);
    }
}
 
IC.c

#include "stm32f10x.h" 
// 初始化输入捕获模块
void IC_Init(void)
{
// 启用TIM3时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// 启用GPIOA时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置GPIO,为上拉输入模式
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; 
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置TIM3为内部时钟
    TIM_InternalClockConfig(TIM3);
// 配置TIM3的时基单元
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; // ARR
    TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; // PSC
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
// 配置TIM3通道1的输入捕获参数
    TIM_ICInitTypeDef TIM_ICInitStructure;
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStructure.TIM_ICFilter = 0xF;
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
    TIM_ICInit(TIM3, &TIM_ICInitStructure);
// 配置TIM3触发源和从模式
    TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
    TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
// 启用TIM3
    TIM_Cmd(TIM3, ENABLE);
}
// 获取输入捕获模块测得的频率
uint32_t IC_GetFreq(void)
{
    return 1000000 / (TIM_GetCapture1(TIM3) + 1);
}
 

PWMI模式测频率占空比

使用STM32自身IO口,PA0输出信号,PA6接收并检测频率和占空比。

main.c

#include "stm32f10x.h" 
//自定义头文件
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
int main(void)
{
// 初始化
    OLED_Init();
    PWM_Init();
    IC_Init();
// OLED显示信息
    OLED_ShowString(1, 1, "Freq:00000Hz");
    OLED_ShowString(2, 1, "Duty:00%");
// 设置PWM的预分频器和占空比
    PWM_SetPrescaler(720 - 1); // Freq = 72M / (PSC + 1) / 100
    PWM_SetCompare1(50);       // Duty = CCR / 100
    while (1)
    {
// 获取输入捕获模块测得的频率和占空比,并在OLED上显示
        OLED_ShowNum(1, 6, IC_GetFreq(), 5); // 显示频率
        OLED_ShowNum(2, 6, IC_GetDuty(), 2); // 显示占空比
    }
}
 
IC.c

#include "stm32f10x.h" // 包含STM32标准库头文件
// 初始化输入捕获模块
void IC_Init(void)
{
// 启用TIM3时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// 启用GPIOA时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置GPIO为上拉输入
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; // 配置的GPIO引脚
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置TIM3为内部时钟
    TIM_InternalClockConfig(TIM3);
// 配置TIM3的时基单元
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; // ARR
    TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; // PSC
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
// 配置TIM3通道1的输入捕获参数
    TIM_ICInitTypeDef TIM_ICInitStructure;
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStructure.TIM_ICFilter = 0xF;
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
    TIM_PWMIConfig(TIM3, &TIM_ICInitStructure); //该函数只需传入一个参数,会自动把剩下的通道初始化为相反的配置,仅适用于通道1和通道2
// 配置TIM3触发源和从模式
    TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
    TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
// 启用TIM3计数器
    TIM_Cmd(TIM3, ENABLE);
}
// 获取输入捕获模块测得的频率
uint32_t IC_GetFreq(void)
{
    return 1000000 / (TIM_GetCapture1(TIM3) + 1);
}
// 获取输入捕获模块测得的占空比
uint32_t IC_GetDuty(void)
{
    return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1);
}
 

编码器接口

编码器接口简介

•Encoder Interface 编码器接口

•编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度

•每个高级定时器和通用定时器都拥有1个编码器接口

•两个输入引脚借用了输入捕获的通道1和通道2(CH1和CH2引脚)

正交编码器

正交编码器可以输出两个相位相差90°的方波信号。接入stm32,可看成一个带有方向控制的外部时钟。

使用正交信号精度更高,相当于AB相都参与计次,提高计次频率的同时,也可以抗干扰。

编码器接口基本结构

工作模式

实例(均不反相)

实例(TI1反相)

如果接入编码器后,需要调整数据加减的方向,可直接调用极性选择。

示例程序

编码器测速

选择上拉输入或下拉输入时,应参考外部模块的默认输出电平,保持一致。防止电平冲突。

main.c

#include "stm32f10x.h"       
#include "Delay.h"             
#include "OLED.h"              
#include "Timer.h"             
#include "Encoder.h"           
//
int16_t Speed;                  
//
int main(void)
{
//初始化
    OLED_Init();               
    Timer_Init();              
    Encoder_Init();            
    OLED_ShowString(1, 1, "Speed:");
    while (1)
    {
        Speed = Encoder_Get();  						 	// 获取编码器的值并存储在Speed变量中
        OLED_ShowSignedNum(1, 7, Speed, 5);   // 在OLED上显示Speed的值
    }
}
//
void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
    {
        Speed = Encoder_Get();                            // 获取编码器的值并存储在Speed变量中
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);       // 清除定时器2更新中断标志位
    }
}
 
Encoder.c

#include "stm32f10x.h"  // 包含STM32标准库的头文件
void Encoder_Init(void)
{
//开启时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);  
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); 
// 配置GPIO引脚模式、速度、和上拉输入
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置TIM3定时器为编码器模式
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1;
    TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
// 配置TIM3的输入捕获通道1和通道2
    TIM_ICInitTypeDef TIM_ICInitStructure;
    TIM_ICStructInit(&TIM_ICInitStructure);
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStructure.TIM_ICFilter = 0xF;
    TIM_ICInit(TIM3, &TIM_ICInitStructure);
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
    TIM_ICInitStructure.TIM_ICFilter = 0xF;
    TIM_ICInit(TIM3, &TIM_ICInitStructure);
// 配置编码器接口
    TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
// 启动TIM3定时器
    TIM_Cmd(TIM3, ENABLE);  
}
//
int16_t Encoder_Get(void)
{
    int16_t Temp;
    Temp = TIM_GetCounter(TIM3);  // 获取TIM3计数器的值
    TIM_SetCounter(TIM3, 0);     // 清零TIM3计数器的值
    return Temp;                  // 返回获取到的值
}
 

ADC

ADC简介

• ADC(Analog-Digital Converter)模拟 - 数字转换器

 DAC则是数字 - 模拟转换器,一般可用PWM平替DAC输出电机所需电压。DAC则可用于波形生成。

• ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁

• 12位逐次逼近型ADC,1us转换时间

• 输入电压范围:0~3.3V,转换结果范围:0~4095

• 18个输入通道,可测量16个外部和2个内部信号源

• 规则组(常规使用)和注入组(突发事件)两个转换单元

• 模拟看门狗自动监测输入电压范围

• STM32F103C8T6 ADC资源:ADC1、ADC2,10个外部输入通道

逐次逼近型ADC

首先在通道选择开关选择一路输入(利用地址锁存和译码选择),再通过比较器进行比较,根据比较结果,改变DAC的值,直到和输入电压近似相等,则DAC的值为相应的数字变量。REF为参考电压。比较时通常用二分法,逐次逼近。

ADC框图

ADC基本结构

触发控制可选择软件触发或者硬件触发。

输入通道

规则组的四种转换模式

单次转换,非扫描模式

将需要转换的通道2写入序列1,触发转换即可,完成后将数据放入寄存器,同时将EOC(标志位)置1。下次转换将再次实行以上步骤。

连续转换,非扫描模式

与单次转换不同,转换结束后将立即开始下一轮转换。即,触发一次,转换会一直进行下去。

单次转换,扫描模式

相较于单次转换,非扫描模式,可填入多个通道进行转换。

连续转换,扫描模式

触发控制

数据对齐

一般使用右对齐,可直接得出结果。也可以使用左对齐,然后裁剪数据分辨率。

转换时间

• AD转换的步骤:采样,保持,量化,编码

• STM32 ADC的总转换时间为:

  TCONV = 采样时间 + 12.5个ADC周期

• 例如:当ADCCLK=14MHz,采样时间为1.5个ADC周期

  TCONV = 1.5 + 12.5 = 14个ADC周期 = 1μs

校准

• ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差

• 建议在每次上电后执行一次校准

• 启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期

硬件电路

示例程序

一般步骤

1.开启RCC时钟,ADC,GPIO。

2.配置GPIO为模拟输入。

3.配置多路开关,将所要转换的通道接入规则组。

4.配置ADC转换器

5.调用ADC_cmd()函数使能ADC。

AD单通道

main.c

#include "stm32f10x.h"  
#include "Delay.h"      
#include "OLED.h"       
#include "AD.h"         
//
uint16_t ADValue;       // 存储AD转换后的数值
float Voltage;          // 存储计算后的电压值
//
int main(void)
{
// 初始化
    OLED_Init();         
    AD_Init();         
    OLED_ShowString(1, 1, "ADValue:");    
    OLED_ShowString(2, 1, "Volatge:0.00V"); 
    while (1)
    {
        ADValue = AD_GetValue();    // 获取AD转换后的数值
        Voltage = (float)ADValue / 4095 * 3.3; // 计算电压值
        // 在OLED上显示AD值和电压值(带小数点)
        OLED_ShowNum(1, 9, ADValue, 4);
        OLED_ShowNum(2, 9, Voltage, 1);
        // 在OLED上显示电压值的小数部分(两位小数)
        OLED_ShowNum(2, 11, (uint16_t)(Voltage * 100) % 100, 2);
        Delay_ms(100);  // 延时100毫秒
        }
}
 
AD.c

#include "stm32f10x.h"  
void AD_Init(void)
{
// 使能ADC1和GPIOA的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置ADC时钟分频
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);
// 配置GPIO
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;  // 配置引脚为模拟输入模式
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;      
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置ADC的通道、采样时间
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
//利用结构体配置ADC
    ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;                       // 独立模式
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;                   // 数据右对齐
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;      // 不使用外部触发
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;                      // 单次转换
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;                						 // 非扫描模式
    ADC_InitStructure.ADC_NbrOfChannel = 1;                      						 // 转换通道数为1
    ADC_Init(ADC1, &ADC_InitStructure);
// 启用ADC1
    ADC_Cmd(ADC1, ENABLE);  
//校准
    ADC_ResetCalibration(ADC1);  // 复位校准寄存器
    while (ADC_GetResetCalibrationStatus(ADC1) == SET); // 等待校准寄存器复位完成
    ADC_StartCalibration(ADC1);  // 启动ADC1校准
    while (ADC_GetCalibrationStatus(ADC1) == SET);  // 等待校准完成
}
//转换函数
uint16_t AD_GetValue(void)
{
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);                    // 启动ADC转换
    while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);    // 等待转换完成
    return ADC_GetConversionValue(ADC1);                       // 获取ADC转换结果
}
 

AD多通道

采用扫描模式实现多通道,最好配合DMA防止数据覆盖。

本次利用单次转换非扫描模式,只需在每次触发转换之前,手动更改一下列表第一个位置通道即可。

main.c

#include "stm32f10x.h"  
#include "Delay.h"      
#include "OLED.h"       
#include "AD.h"        
// 存储四个ADC通道的转换值
uint16_t AD0, AD1, AD2, AD3; 
int main(void)
{
// 初始化
    OLED_Init();   
    AD_Init();     
    OLED_ShowString(1, 1, "AD0:"); 
    OLED_ShowString(2, 1, "AD1:"); 
    OLED_ShowString(3, 1, "AD2:"); 
    OLED_ShowString(4, 1, "AD3:"); 
    while (1)
    {
        // 获取ADC通道的转换值
        AD0 = AD_GetValue(ADC_Channel_0); 
        AD1 = AD_GetValue(ADC_Channel_1); 
        AD2 = AD_GetValue(ADC_Channel_2); 
        AD3 = AD_GetValue(ADC_Channel_3); 
     // 在OLED上显示
        OLED_ShowNum(1, 5, AD0, 4);
        OLED_ShowNum(2, 5, AD1, 4);
        OLED_ShowNum(3, 5, AD2, 4);
        OLED_ShowNum(4, 5, AD3, 4);
     // 延时100毫秒
        Delay_ms(100); 
    }
}
 
AD.c

#include "stm32f10x.h"
void AD_Init(void)
{
// 使能ADC1和GPIOA的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置ADC时钟分频
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);
// 配置GPIO引脚为模拟输入模式
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;  
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; 
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置ADC的通道、采样时间等参数
    ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;                       // 独立模式
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;                  // 数据右对齐
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;    // 不使用外部触发
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;                    // 单次转换
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;                         // 非扫描模式
    ADC_InitStructure.ADC_NbrOfChannel = 1;                               // 转换通道数为1
    ADC_Init(ADC1, &ADC_InitStructure);
// 启用ADC1
    ADC_Cmd(ADC1, ENABLE);  
//校准
    ADC_ResetCalibration(ADC1);  													// 复位校准寄存器
    while (ADC_GetResetCalibrationStatus(ADC1) == SET); 	// 等待校准寄存器复位完成
    ADC_StartCalibration(ADC1);  													// 启动ADC1校准
    while (ADC_GetCalibrationStatus(ADC1) == SET);  			// 等待校准完成
}
//
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
    ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5);	// 配置ADC
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);  									// 启动ADC转换
    while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); 					// 等待转换完成
    return ADC_GetConversionValue(ADC1);  										// 获取ADC转换结果
}
 

DMA

DMA简介

• DMA(Direct Memory Access)直接存储器存取

• DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源(可直接访问32内部存储器,包括内存SRAM,Flash)

• 12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)

• 每个通道都支持软件触发和特定的硬件触发

• STM32F103C8T6 DMA资源:DMA1(7个通道)

存储器映像

DMA框图

DMA基本结构

DMA无法进行SRAM到Flash,Flash到Flash操作。M2M用于选择硬件触发或软件触发,EN位为DMA的开关控制。

DMA请求

使用硬件触发需选择专门的通道,选择软件触发则可任意选择。

数据宽度与对齐

源端宽度=目标宽度,数据正常传输。

源端宽度<目标宽度,目标前面空位补零。

源端宽度>目标宽度,舍弃源端高位。

数据转运+DMA

将DataA转运到DataB,外设地址给DataA数组地址,存储器地址给DataB的首地址,宽度都为8位传输,且两地址都自增,传输计数器赋值为7,无需自动重装,使用软件触发,调用DMA_cmd();

ADC扫描模式+DMA

示例程序

DMA数据转运

main.c

#include "stm32f10x.h"     // 包含STM32标准库的头文件
#include "Delay.h"         // 包含自定义延时函数的头文件
#include "OLED.h"          // 包含OLED显示屏的头文件
#include "MyDMA.h"         // 包含自定义DMA初始化和传输函数的头文件
// 定义两个数组,DataA和DataB,分别用于存储数据
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
uint8_t DataB[] = {0, 0, 0, 0};
int main(void)
{
// 初始化OLED显示屏
    OLED_Init();
// 使用自定义的DMA初始化函数初始化DMA,并配置数据源和目标地址
    MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);
// 在OLED上显示文本和数据的初始状态
    OLED_ShowString(1, 1, "DataA");
    OLED_ShowString(3, 1, "DataB");
    OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);
    OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);
    while (1)
    {
// 修改DataA数组的值
        DataA[0]++;
        DataA[1]++;
        DataA[2]++;
        DataA[3]++;
// 在OLED上显示DataA和DataB的各个元素的值
        OLED_ShowHexNum(2, 1, DataA[0], 2);
        OLED_ShowHexNum(2, 4, DataA[1], 2);
        OLED_ShowHexNum(2, 7, DataA[2], 2);
        OLED_ShowHexNum(2, 10, DataA[3], 2);
        OLED_ShowHexNum(4, 1, DataB[0], 2);
        OLED_ShowHexNum(4, 4, DataB[1], 2);
        OLED_ShowHexNum(4, 7, DataB[2], 2);
        OLED_ShowHexNum(4, 10, DataB[3], 2);    
// 使用自定义的延时函数延时1秒
        Delay_ms(1000);   
// 使用DMA传输数据
        MyDMA_Transfer();
// 再次在OLED上显示DataA和DataB的各个元素的值
        OLED_ShowHexNum(2, 1, DataA[0], 2);
        OLED_ShowHexNum(2, 4, DataA[1], 2);
        OLED_ShowHexNum(2, 7, DataA[2], 2);
        OLED_ShowHexNum(2, 10, DataA[3], 2);
        OLED_ShowHexNum(4, 1, DataB[0], 2);
        OLED_ShowHexNum(4, 4, DataB[1], 2);
        OLED_ShowHexNum(4, 7, DataB[2], 2);
        OLED_ShowHexNum(4, 10, DataB[3], 2);
// 使用自定义的延时函数延时1秒
        Delay_ms(1000);
    }
}
 
MyDMA.c

#include "stm32f10x.h"  
// 定义一个全局变量,用于存储DMA传输的数据大小
uint16_t MyDMA_Size; 
// DMA初始化函数,用于配置DMA通道
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
    MyDMA_Size = Size;                                      // 将传入的数据大小保存到全局变量中
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);     // 启用DMA1时钟
//结构体配置DMA
    DMA_InitTypeDef DMA_InitStructure;
    DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;                          // 配置外设基地址
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;    // 配置外设数据大小为字节
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;            // 允许外设地址递增
    DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;                              // 配置内存基地址
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;            // 配置内存数据大小为字节
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;                    // 允许内存地址递增
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;                         // 数据传输方向:从外设到内存
    DMA_InitStructure.DMA_BufferSize = Size;                                   // 配置DMA传输的数据大小
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;                              // DMA工作模式为普通模式
    DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;                                // 启用内存到内存传输
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;                      // 配置DMA优先级为中等
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);                               // 使用通道1初始化DMA
// 禁用DMA通道1,等待启动传输
    DMA_Cmd(DMA1_Channel1, DISABLE); 
}
// DMA传输函数,用于启动DMA传输
void MyDMA_Transfer(void)
{
    DMA_Cmd(DMA1_Channel1, DISABLE);                         // 禁用DMA通道1,准备重新配置
    DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);       // 配置当前数据传输计数器的值
    DMA_Cmd(DMA1_Channel1, ENABLE);                          // 启用DMA通道1,开始传输
    while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);       // 等待传输完成,检查传输完成标志
    DMA_ClearFlag(DMA1_FLAG_TC1);                            // 清除传输完成标志
}
 

DMA+AD多通道

main.c

#include "stm32f10x.h" 
#include "AD.h"        
#include "OLED.h"      
#include "Delay.h"     
int main(void)
{
// 初始化	
    OLED_Init();     
    AD_Init();       
    OLED_ShowString(1, 1, "AD0:");
    OLED_ShowString(2, 1, "AD1:");
    OLED_ShowString(3, 1, "AD2:");
    OLED_ShowString(4, 1, "AD3:");
    while (1)
    {
// 读取ADC模块的四个通道(AD0、AD1、AD2、AD3)的值,并显示在OLED屏上
        OLED_ShowNum(1, 5, AD_Value[0], 4);
        OLED_ShowNum(2, 5, AD_Value[1], 4);
        OLED_ShowNum(3, 5, AD_Value[2], 4);
        OLED_ShowNum(4, 5, AD_Value[3], 4);
// 延时100毫秒
        Delay_ms(100); 
    }
}
 
AD.c

#include "stm32f10x.h" 
// 用于存储ADC模块的转换结果
uint16_t AD_Value[4]; 
void AD_Init(void)
{
// 启用ADC1、GPIOA和DMA1的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置ADC时钟分频
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);
// 配置ADC通道对应的GPIO引脚为模拟输入模式
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置ADC通道和采样时间
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);
// 配置ADC工作模式和DMA模式
    ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
    ADC_InitStructure.ADC_ScanConvMode = ENABLE;
    ADC_InitStructure.ADC_NbrOfChannel = 4;
    ADC_Init(ADC1, &ADC_InitStructure);
// 配置DMA通道,用于传输ADC数据
    DMA_InitTypeDef DMA_InitStructure;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = 4;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);
// 启用DMA通道
    DMA_Cmd(DMA1_Channel1, ENABLE);
// 启用ADC的DMA传输
    ADC_DMACmd(ADC1, ENABLE);
// 启用ADC
    ADC_Cmd(ADC1, ENABLE);
// 复位ADC校准
    ADC_ResetCalibration(ADC1);
    while (ADC_GetResetCalibrationStatus(ADC1) == SET);
// 开始ADC校准
    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1) == SET);
// 启动ADC转换
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
 

USART串口

通信接口

• 通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统

• 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发

引脚:
TX:数据发送引脚

RX:数据接收引脚

SCL:时钟线

SDA:数据线

SCLK:时钟

MOSI:主机输出数据脚

MISO:主机输入数据脚

CS:片选,用于指定通信对象

CAN和USB:表中的双引脚用于表示差分信号。

双工:

全双工:双方可同时互相发送和接受信息。

半双工:任意一方发送的时候,另一方只能接收。

单工:只能一方发送,另一方接收,单向传输。

时钟:

同步:双方通信配有时钟线

异步:自定义时钟

单端:引脚的高低电平为GND的电压差,所以双方需要共地。

硬件电路

• 简单双向串口通信有两根通信线(发送端TX和接收端RX)

• TX与RX要交叉连接

• 当只需单向的数据传输时,可以只接一根通信线

• 当电平标准不一致时,需要加电平转换芯片

电平标准

电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:

• TTL电平:+3.3V或+5V表示1,0V表示0

• RS232电平:-3 ~ -15V表示1,+3 ~ +15V表示0

• RS485电平:两线压差+2 ~ +6V表示1,-2 ~ -6V表示0(差分信号)

串口参数及时序

• 波特率:串口通信的速率

• 起始位:标志一个数据帧的开始,固定为低电平

• 数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行

• 校验位:用于数据验证,根据数据位计算得来

• 停止位:用于数据帧间隔,固定为高电平

右图RB8/TB8为奇偶校验位。

串口时序

各种情况下,串口发送数据的时序。

USART外设简介

• USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器

• USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里

• 自带波特率发生器,最高达4.5Mbits/s(可理解为分频器)

• 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)

• 可选校验位(无校验/奇校验/偶校验)

• 支持同步模式(CLK时钟输出)、硬件流控制、DMA、智能卡、IrDA、LIN

• STM32F103C8T6 USART资源: USART1、 USART2、 USART3

USART框图

USART基本结构

数据帧

起始位侦测

数据采样

波特率发生器

• 发送器和接收器的波特率由波特率寄存器BRR里的DIV确定

• 计算公式:波特率 = fPCLK2/1 / (16 * DIV)

数据模式

• HEX模式/十六进制模式/二进制模式:以原始数据的形式显示

• 文本模式/字符模式:以原始数据编码后的形式显示

image-20230926211002416

示例程序

串口发送/接收数据

main.c

#include "stm32f10x.h"                  
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
//定义接收数据
uint8_t RxData;
int main(void)
{
//初始化数据
    OLED_Init();
    OLED_ShowString(1, 1, "RxData:");
    Serial_Init();
    while (1)
    {
//实现功能为数据互传,当上位机发送数据,从机接收到数据时,将收到的数据回传发送。
        if (Serial_GetRxFlag() == 1)
        {
            RxData = Serial_GetRxData();
            Serial_SendByte(RxData);
            OLED_ShowHexNum(1, 8, RxData, 2);
        }
    }
}
 
Serial.c

#include "stm32f10x.h"                  // Device header
#include 
#include 
//定义临时变量
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;
//初始化函数
void Serial_Init(void)
{
//开启GPIO和USART的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//初始化GPIO
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;   //复用推挽输出模式
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
//用于接收
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;  //上拉输入模式
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置USART
    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 9600;    //配置波特率
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;  //不使用硬件流控制
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;  //配置USART为发送和接收模式
    USART_InitStructure.USART_Parity = USART_Parity_No;   //配置校验位,无校验
    USART_InitStructure.USART_StopBits = USART_StopBits_1;   //停止位为1位
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;  //子长为8字节
    USART_Init(USART1, &USART_InitStructure);
//开启USART中断
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
//用NVIC进行分组
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//配置中断
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);
//使能USART
    USART_Cmd(USART1, ENABLE);
}
//发送一个字节,调用标准库函数发送字节,并判断标志位等待发送。
void Serial_SendByte(uint8_t Byte)  
{
    USART_SendData(USART1, Byte);
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
//发送一个数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
    uint16_t i;
    for (i = 0; i < Length; i ++)
    {
        Serial_SendByte(Array[i]);
    }
}
//发送字符串
void Serial_SendString(char *String)
{
    uint8_t i;
    for (i = 0; String[i] != '\0'; i ++)
    {
        Serial_SendByte(String[i]);
    }
}
//求平方
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
    uint32_t Result = 1;
    while (Y --)
    {
        Result *= X;
    }
    return Result;
}
//发送数字
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
    uint8_t i;
    for (i = 0; i < Length; i ++)
    {
        Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
    }
}
//printf调用fputc打印,此函数使fputc为发送到串口。
int fputc(int ch, FILE *f)
{
    Serial_SendByte(ch);
    return ch;
}
//printf重定向,用于在程序中,调用printf,可打印数据至串口
void Serial_Printf(char *format, ...)
{
    char String[100];
    va_list arg;
    va_start(arg, format);
    vsprintf(String, format, arg);
    va_end(arg);
    Serial_SendString(String);
}
//自定义函数,获取读标志位
uint8_t Serial_GetRxFlag(void)
{
    if (Serial_RxFlag == 1)
    {
        Serial_RxFlag = 0;
        return 1;
    }
    return 0;
}
//自定义函数,获取读标志位
uint8_t Serial_GetRxData(void)
{
    return Serial_RxData;
}
//USART中断,读取接收数据
void USART1_IRQHandler(void)
{
    if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
    {
        Serial_RxData = USART_ReceiveData(USART1);
        Serial_RxFlag = 1;
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}
 

具体函数功能已有注释,值得一提的是发送数字的功能,除了示例程序中,求数字次方再提取数字(此方法在博客C语言循环结构中的题目《计数问题》有更好的处理)。也可以利用发送字符串的功能。利用C语言函数,可将数字转换为字符串再发送。当然,别忘了引入C语言头文件<stdio.h>。

1
2
3
4
char VoltageStr[10];  // 定义一个足够大的字符数组来存储转换后的字符串    
Num = 1245;
sprintf(VoltageStr, "%d", Num); // 使用sprintf将整数转换为字符串
SendString(VoltageStr); // 发送字符串

数据包

HEX数据包

文本数据包

HEX数据包接收

文本数据包接收

在程序设计中,需设计一个能记住不同状态的机制,称为状态机。

示例程序

串口收发HEX数据包

main.c

#include "stm32f10x.h"  
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "Key.h"
// 用于存储按键的状态
uint8_t KeyNum;  
int main(void)
{
//初始化
    OLED_Init();   
    Key_Init();    
    Serial_Init(); 
    OLED_ShowString(1, 1, "TxPacket"); 
    OLED_ShowString(3, 1, "RxPacket");
// 预设串口发送数据的内容
    Serial_TxPacket[0] = 0x01; 
    Serial_TxPacket[1] = 0x02;
    Serial_TxPacket[2] = 0x03;
    Serial_TxPacket[3] = 0x04;
    while (1)
    {
//获取按键状态
        KeyNum = Key_GetNum(); 
// 如果按键被按下,将递增串口发送数据的内容
        if (KeyNum == 1) 
        {
            Serial_TxPacket[0]++;
            Serial_TxPacket[1]++;
            Serial_TxPacket[2]++;
            Serial_TxPacket[3]++;
// 发送串口数据
            Serial_SendPacket(); 
// 在OLED上显示发送数据的十六进制值
            OLED_ShowHexNum(2, 1, Serial_TxPacket[0], 2);
            OLED_ShowHexNum(2, 4, Serial_TxPacket[1], 2);
            OLED_ShowHexNum(2, 7, Serial_TxPacket[2], 2);
            OLED_ShowHexNum(2, 10, Serial_TxPacket[3], 2);
        }
// 如果收到串口接收标志位
        if (Serial_GetRxFlag() == 1) 
        {
// 在OLED上显示接收到的数据的十六进制值
            OLED_ShowHexNum(4, 1, Serial_RxPacket[0], 2);
            OLED_ShowHexNum(4, 4, Serial_RxPacket[1], 2);
            OLED_ShowHexNum(4, 7, Serial_RxPacket[2], 2);
            OLED_ShowHexNum(4, 10, Serial_RxPacket[3], 2);
        }
    }
}
 
Serial.c

#include "stm32f10x.h"  // 包含STM32的设备头文件
#include 
#include 
//定义数据
uint8_t Serial_TxPacket[4];  // 用于存储串口发送的数据
uint8_t Serial_RxPacket[4];  // 用于存储串口接收的数据
uint8_t Serial_RxFlag;       // 串口接收标志位
// 初始化串口
void Serial_Init(void)
{
// 使能USART1和GPIOA的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置GPIO发送端口
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;         // USART1的TX引脚
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置GPIO接收端口
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;        // USART1的RX引脚
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置USART
    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 9600;         // 波特率设置为9600
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_Init(USART1, &USART_InitStructure);
// 使能USART1的接收中断
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);     
// 设置NVIC的优先级分组为组2
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);    
// 配置USART1的中断
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;  
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);
// 使能USART1
    USART_Cmd(USART1, ENABLE);                         
}
// 发送单个字节
void Serial_SendByte(uint8_t Byte)
{
    USART_SendData(USART1, Byte);
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
// 发送字节数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
    uint16_t i;
    for (i = 0; i < Length; i ++)
    {
        Serial_SendByte(Array[i]);
    }
}
// 发送字符串
void Serial_SendString(char *String)
{
    uint8_t i;
    for (i = 0; String[i] != '\0'; i ++)
    {
        Serial_SendByte(String[i]);
    }
}
// 计算X的Y次方
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
    uint32_t Result = 1;
    while (Y --)
    {
        Result *= X;
    }
    return Result;
}
// 发送数字
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
    uint8_t i;
    for (i = 0; i < Length; i ++)
    {
        Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
    }
}
// 重定向printf函数
int fputc(int ch, FILE *f)
{
    Serial_SendByte(ch);
    return ch;
}
// 带格式的字符串发送
void Serial_Printf(char *format, ...)
{
    char String[100];
    va_list arg;
    va_start(arg, format);
    vsprintf(String, format, arg);
    va_end(arg);
    Serial_SendString(String);
}
// 发送串口数据包
void Serial_SendPacket(void)
{
    Serial_SendByte(0xFF);                 //数据包包头
    Serial_SendArray(Serial_TxPacket, 4);  //数据包数据
    Serial_SendByte(0xFE);                 //数据包包尾
}
// 获取串口接收标志位
uint8_t Serial_GetRxFlag(void)
{
    if (Serial_RxFlag == 1)
    {
        Serial_RxFlag = 0;
        return 1;
    }
    return 0;
}
// USART1的中断处理函数,内有状态机,处理数据包各种情况
void USART1_IRQHandler(void)
{
    static uint8_t RxState = 0;
    static uint8_t pRxPacket = 0;
    if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
    {
        uint8_t RxData = USART_ReceiveData(USART1);
        if (RxState == 0)
        {
            if (RxData == 0xFF)
            {
                RxState = 1;
                pRxPacket = 0;
            }
        }
        else if (RxState == 1)
        {
            Serial_RxPacket[pRxPacket] = RxData;
            pRxPacket ++;
            if (pRxPacket >= 4)
            {
                RxState = 2;
            }
        }
        else if (RxState == 2)
        {
            if (RxData == 0xFE)
            {
                RxState = 0;
                Serial_RxFlag = 1;
            }
        }
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}
 
Serial.h

#ifndef __SERIAL_H
#define __SERIAL_H
//
#include 
//定义变量时用extern,可被主函数调用。
extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];
//
void Serial_Init(void);                                          //初始化
void Serial_SendByte(uint8_t Byte);                              //发送字节
void Serial_SendArray(uint8_t *Array, uint16_t Length);          //发送数组
void Serial_SendString(char *String);                            //发送字符串
void Serial_SendNumber(uint32_t Number, uint8_t Length);         //发送数字
void Serial_Printf(char *format, ...);                           //打印至串口
void Serial_SendPacket(void);                                    //发送数据包
uint8_t Serial_GetRxFlag(void);                                  //获取接收标志位
//
#endif
 

这里引入.h文件是因为定义到Sericl.c的变量,需要在main.c调用。

各函数功能和串口收发数据区别不大,但在中断函数中引入的状态机,先等待包头,收到包头后开启接受数据的函数,并保存至数组。接受完毕后等待包尾,完成一个数据包的传输。

主函数的功能是按下按钮时,将发送以“FF 02 03 04 05 FE”为格式的数据包,按键每按下一次,数据包内的数据将自增1。也可以用同样的格式发送数据包,发送和接受的数据包的数据(不含包头包尾)将在OLED显示。

串口收发文本数据包

main.c

#include "stm32f10x.h"  
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "LED.h"
#include "string.h"
int main(void)
{
//初始化
    OLED_Init();        
    LED_Init();         
    Serial_Init();      
    OLED_ShowString(1, 1, "TxPacket");
    OLED_ShowString(3, 1, "RxPacket");
    while (1)
    {
        if (Serial_RxFlag == 1)  // 如果收到串口接收标志位
        {
            OLED_ShowString(4, 1, "                ");  // 清除OLED上的数据
            OLED_ShowString(4, 1, Serial_RxPacket);     // 在OLED上显示收到的数据
            if (strcmp(Serial_RxPacket, "LED_ON") == 0)  // 如果收到 "LED_ON" 命令
            {
                LED1_ON();  // 打开LED1
                Serial_SendString("LED_ON_OK\r\n");         // 发送成功消息给串口
                OLED_ShowString(2, 1, "                ");  // 清除OLED上的数据
                OLED_ShowString(2, 1, "LED_ON_OK");         // 在OLED上显示 "LED_ON_OK"
            }
            else if (strcmp(Serial_RxPacket, "LED_OFF") == 0)  // 如果收到 "LED_OFF" 命令
            {
                LED1_OFF();  // 关闭LED1
                Serial_SendString("LED_OFF_OK\r\n");         // 发送成功消息给串口
                OLED_ShowString(2, 1, "                ");   // 清除OLED上的数据
                OLED_ShowString(2, 1, "LED_OFF_OK");         // 在OLED上显示 "LED_OFF_OK"
            }
            else
            {
                Serial_SendString("ERROR_COMMAND\r\n");      // 发送错误消息给串口
                OLED_ShowString(2, 1, "                ");   // 清除OLED上的数据
                OLED_ShowString(2, 1, "ERROR_COMMAND");      // 在OLED上显示 "ERROR_COMMAND"
            }
            Serial_RxFlag = 0;  // 重置串口接收标志位
        }
    }
}
 
Serial.c

#include "stm32f10x.h"  
#include 
#include 
//
char Serial_RxPacket[100]; // 存储串口接收的数据,最大长度为100
uint8_t Serial_RxFlag;     // 串口接收标志位
// 初始化串口
void Serial_Init(void)
{
// 使能USART1和GPIOA的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置GPIO发送端
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;      // USART1的TX引脚
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置GPIO接收端
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;     // USART1的RX引脚
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置USART
    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 9600;      // 波特率设置为9600
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_Init(USART1, &USART_InitStructure);
// 使能USART1的接收中断
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);  
// 设置NVIC的优先级分组为组2
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 
// 配置USART1的中断
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; 
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);
// 使能USART1
    USART_Cmd(USART1, ENABLE);                      
}
// 发送单个字节
void Serial_SendByte(uint8_t Byte)
{
    USART_SendData(USART1, Byte);
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
// 发送字节数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
    uint16_t i;
    for (i = 0; i < Length; i ++)
    {
        Serial_SendByte(Array[i]);
    }
}
// 发送字符串
void Serial_SendString(char *String)
{
    uint8_t i;
    for (i = 0; String[i] != '\0'; i ++)
    {
        Serial_SendByte(String[i]);
    }
}
// 计算X的Y次方
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
    uint32_t Result = 1;
    while (Y --)
    {
        Result *= X;
    }
    return Result;
}
// 发送数字
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
    uint8_t i;
    for (i = 0; i < Length; i ++)
    {
        Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
    }
}
// 重定向printf函数
int fputc(int ch, FILE *f)
{
    Serial_SendByte(ch);
    return ch;
}
// 带格式的字符串发送
void Serial_Printf(char *format, ...)
{
    char String[100];
    va_list arg;
    va_start(arg, format);
    vsprintf(String, format, arg);
    va_end(arg);
    Serial_SendString(String);
}
// USART1的中断处理函数,内有状态机,处理数据包各种情况
void USART1_IRQHandler(void)
{
    static uint8_t RxState = 0;
    static uint8_t pRxPacket = 0;
    if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
    {
        uint8_t RxData = USART_ReceiveData(USART1);
        if (RxState == 0)
        {
            if (RxData == '@' && Serial_RxFlag == 0) // 如果接收到'@'字符且标志位为0
            {
                RxState = 1;
                pRxPacket = 0;
            }
        }
        else if (RxState == 1)
        {
            if (RxData == '\r') // 如果接收到'\r'字符
            {
                RxState = 2;
            }
            else
            {
                Serial_RxPacket[pRxPacket] = RxData; // 存储接收到的字符
                pRxPacket ++;
            }
        }
        else if (RxState == 2)
        {
            if (RxData == '\n') // 如果接收到'\n'字符
            {
                RxState = 0;
                Serial_RxPacket[pRxPacket] = '\0'; // 在末尾添加字符串结束符
                Serial_RxFlag = 1; // 设置串口接收标志位为1,表示接收完成
            }
        }
//清除标志位
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}
 
Serial.h

#ifndef __SERIAL_H
#define __SERIAL_H
#include 
// 定义串口接收缓冲区和接收标志位,这些变量可在main中引用
extern char Serial_RxPacket[];
extern uint8_t Serial_RxFlag;
//
void Serial_Init(void);                                  // 串口初始化函数
void Serial_SendByte(uint8_t Byte);                      // 发送单个字节到串口
void Serial_SendArray(uint8_t *Array, uint16_t Length);  // 发送字节数组到串口
void Serial_SendString(char *String);                    // 发送字符串到串口
void Serial_SendNumber(uint32_t Number, uint8_t Length); // 发送一个数字到串口,Length 表示数字的位数
void Serial_Printf(char *format, ...);                   // 带格式的字符串发送,类似于 printf
//
#endif
 

和HEX数据包类似,只是将数据形式换成了字符型。就不做过多说明。

main函数实现的功能为,在上位机输入‘@’为包头,输入‘LED_ON’为数据,换行为包尾,发送后将点亮LED,并回传“LED_ON_OK”,输入‘LED_OFF’为数据,发送后将熄灭LED,并回传“LED_OFF_OK”。输入其它指令将无法控制LED,并收到回传“ERROE_COMMAND”。

FlyMcu程序烧录

串口下载原理

写入的代码通过软件生成.hex文件后,配置BOOT引脚,使STM32执行BootLoader程序,设置BOOT0 = 1并按下复位键,等程序下载完,此时仍在执行下载程序,所以需置BOOT0 = 0,再次按下复位键。

如果stm32没有设置读保护,则写入的程序可被软件读出来,当然,一般只能是hex一类,反编译源码还是不现实。