前言

此笔记有参考了各博主文章:

https://blog.csdn.net/Zach_z/article/details/80548423

STM32应用(六)一阶卡尔曼滤波代码和简单应用

https://blog.csdn.net/Zach_z/article/details/80548423

https://recclay.blog.csdn.net/article/details/86594683

https://zhuanlan.zhihu.com/p/489165065

关于STM32中的引脚重映射

蓝桥杯嵌入式比赛知识点合集

手册:

在阿里云盘—我的手册(已凃)

所有模块总例程

板子介绍

STM32F103RBT6属于中容量,闪存128K,64脚的增强型芯片

  • 查看英文手册 3.1章47页 系统架构

APB2 APB1
ADC1,2,3 DAC,PWR,BKP,bxCAN,IWDG,USB,WWDG
GPIOA,B,C,D,E,F,G UART4,5(只能异步)
USART1(支持同步/异步) USART2,3(支持同步/异步)
SPI1 RTC
TIM1,8 TIM2,3,4,5,6,7
EXTI I2C1,I2C2
AFIO SPI2/I2S,SPI3/I2

蓝桥杯嵌入式组竞赛大纲

配置环境

  • 首先把板子连接到电脑,然后在左下角搜索栏搜索【设备管理器】,打开找到【其他设备】那栏可以看到一个有感叹号的端口号,这时右键它选择【更新驱动程序】,再选择【浏览我的电脑以查找驱动程序】,然后点击【浏览】,定位到你的FT2232驱动文件中,再点击【下一页】即可安装。

  • 安装成功后会看到【端口】栏有一个“USB Serial Port(COMx)”(x是你当前端口号),然后在下面【通用串行总线控制器】有“USB Serial Converter A”和“USB Serial Converter B”(注意:不要把线插到CN5,要插到CN2

  • 然后安装 Colink for MDK插件,默认安装路径(不要改),安装完打开 keil 设置

  • 然后点击旁边的【Settings】进去添加下面这条(如果找不到下面这条说明得去下载)下载地址:MDK v4 传统支持 (keil.com),安装5.25版本

  • 接下来就可以自己创建新项目把蓝桥杯的例程复制进去下载程序到开发板了(注意:不要直接打开官方LCD例程进行下载,不然会一直卡死

  • 先在自己盘符创建5个文件夹如下把需要的文件包含进来,创建项目需要注意选芯片时顶部选"Device Database"–>“STM32F103RB”–>“OK”–>“弹窗的话选否”
文件名 包含 \官方资料…\STM32F10x_StdPeriph_Lib_V3.5.0 下的
APP 自己编写的驱动文件(.c,.h)放这
USER 自己编写的 main.c
\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x 下的
①stm32f10x.h ②system_stm32f10x.c/.h
\Project\STM32F10x_StdPeriph_Template  下的
③stm32f10x_conf.h ④stm32f10x_it.c/.h
CORE \Libraries\CMSIS\CM3\CoreSupport 下的
①core_cm3.c/.h
\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm 下的
②startup_stm32f10x_md.s
GJK \Libraries\STM32F10x_StdPeriph_Driver 下的
《inc》《src》文件夹
project 创建工程保存在这,生成的啥乱七八糟的也放这
  • 点击 三色正方形 创建如上4个文件夹(project不用),把里面所有 .c文件Add进来即可(CORE需要把md.s也包进来) ,点击 魔法棒C/C++Include Paths – 把4个文件夹的路径包含进来(GJK只需要包含《inc》)
  • 魔法棒C/C++Define --填写下面这句即可
//可在 stm32f10x.h 第95,99行找到
USE_STDPERIPH_DRIVER,STM32F10X_MD
  • 魔法棒ARM Compiler – 选择《Use default compiler version 5》(选6可能报错!!!),如果用到C库可把下面的《Use MicroLIB》也勾了
  • 如果编译还错误,左侧展开 APP 下任意一个 .c 文件,打开 stm32f10x_conf.h,看看是不是头文件被注释了,如果是则全部取消注释即可

LED模块

  • 通过原理图可以知道 PC8~PC15 分别对应 LED1~LED8PD2 对应 LE(锁存器),只要使能 LE并且给 PC8–PC15一个低电平,LED1–LED8 就会产生电压差就会亮。
LED STM32
(LED1-LED8)M_PC8~M_PC15 PC8~PC15
(锁存器)M_PD2 PD2

锁存器芯片介绍

  • 74HC573芯片缺口方向 为正,然后左上往下排列,最后一个是GND,然后 向右,再向右上,右上角为Vcc(GND 和Vcc成对角线,是为了防止发生短路)
  • LE:latch-enable 锁存使能 当LE是高电平,Q(output)与D(data)保持一致当LE是低电平,Q保持住原先状态,不受D的影响
  • OE:output enable 输出使能其上有一杠,表示低电平有效故OE必须在低电平状态才行

  • Z是高阻态的意思接在高电平上就是高电平,接在低电平上就是低电平,此时表示芯片不工作。

编写程序

  • 注意需要把 stm32f10x_it.c 里第137到140行注释掉,否则会报错

  • 时钟配置函数可以去 stm32f10x_rcc.c1095行左右
  • 我们想配置 Pin8~Pin15,我们可以直接0x0100+0x0200+0x0400+0x0800+0x1000+0x2000+0x4000+0x8000=0xFF00

  • 开发板复位后, LED默认是点亮的,所以需要初始化时全部设置为高电平
  • 先开锁存器还是先给IO数据呢?

只要有一个 从低到高的脉冲,就会直接把输入端的数据给输出端,所以更准确来说 应该在开锁存器前把数据准备好,然后再拉高LE,锁存完毕后再拉低LE为下次锁存做准备,连续拉高和拉低,之间的间隔都是 ns 级别,足够锁存完数据

/*************************GPIO_init.h*************************/
# define LED1 GPIO_Pin_8	//LED1~8(C)
# define LED2 GPIO_Pin_9
# define LED3 GPIO_Pin_10
# define LED4 GPIO_Pin_11
# define LED5 GPIO_Pin_12
# define LED6 GPIO_Pin_13
# define LED7 GPIO_Pin_14
# define LED8 GPIO_Pin_15
# define LEDALL GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15	//所有LED
# define LE GPIO_Pin_2	//LE锁存器(D)

/*************************GPIO_init.c*************************/
void LEDInit(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;	//定义结构体
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOD,ENABLE);	//使能C,D组外设时钟
	GPIO_InitStructure.GPIO_Pin = 0xFF00;	//PC8~PC15
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;	//推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Pin = LE;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;	//推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOD,&GPIO_InitStructure);
	
	//初始化LED为灭的状态
	GPIO_SetBits(GPIOD,LE);	//开锁
	GPIO_SetBits(GPIOC,0xFF00);	//置1(灭)
	GPIO_ResetBits(GPIOD,LE);	//关锁
}

/*************************led.h*************************/
# define LE GPIO_Pin_2	//LE锁存器(D)

void LEDInit(void);

/*************************led.c*************************/
void delay_us(unsigned int us);
extern void delay_ms(unsigned int ms);

//点亮任意个LED	参数:点亮哪几个(十六进制)
void LED_display_1(unsigned char LED)
{
    GPIO_ResetBits(GPIOC, LED << 8);
    GPIO_SetBits(GPIOD, LE);
    GPIO_ResetBits(GPIOD, LE);
}
//灭掉任意个LED	参数:灭哪几个(十六进制)
void LED_undisplay_1(unsigned char LED)
{
    GPIO_SetBits(GPIOC, LED << 8);
    GPIO_SetBits(GPIOD, LE);
    GPIO_ResetBits(GPIOD, LE);
}

//左右流水	参数:方向(0-左边 1-右边)
void LED_display_2(unsigned char dir)
{
    char i = 0;
    if(dir == 0)
    {
        for(i = 0; i < 8; i++)
        {
            LED_display_1(0x01 << i);
            delay_ms(1000);
            LED_undisplay_1(0x01 << i);
        }
    }
    else if(dir == 1)
    {
        for(i = 0; i < 8; i++)
        {
            LED_display_1(0x80 >> i);
            delay_ms(1000);
            LED_undisplay_1(0x80 >> i);
        }
    }
}

/*************************main.c*************************/
//略(直接调用即可)

KEY

按键 STM32
(B1)N_K1 PA0
(B2)N_K2 PA8
(B3)N_K3 PB1
(B4)N_K4 PB2

方式1(使用延时方式)

key.h

# ifndef __KEY_H
# define __KEY_H
# include "stm32f10x.h"

//注意是读取输入状态不是输出状态!!!
# define KEY1 GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)
# define KEY2 GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_8)
# define KEY3 GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)
# define KEY4 GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_2)

void KEY_Init(void);//初始化
u8 KEY_scan(void);//判断是否按下
void KEY_show1(void);//单次按没锁存
void KEY_show2(void);//单次按有锁存
void KEY_long(void);//长按


# endif
  • 关于内部上下拉电阻的设置:如果外部的按键另一头接地,那么需要设置成上拉电阻。(理由是当没有按下按键时,由于上拉,输入为高电平按下时,由于外部接地,输入为低电平),同理,如果外部的按键另一头接高电平,那么需要设置成下拉电阻。

key.c

# include "key.h"
# include "delay.h"
# include "led.h"

void KEY_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); //开启时钟
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_8; //k1,k2
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2; //k3,k4
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
    GPIO_Init(GPIOB, &GPIO_InitStructure);
}

//返回对应键值
u8 KEY_scan(void)
{


    if(KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0) //读按键接口的电平
    {
        delay_ms(20);//延时消抖

        if(KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0)
        {

            if(KEY1 == 0)
                return 1;
            if(KEY2 == 0)
                return 2;
            if(KEY3 == 0)
                return 3;
            if(KEY4 == 0)
                return 4;
        }
    }
    return 0;
}

//单次按没锁存
void KEY_show1(void)
{
    if(!KEY1)  //读按键接口的电平
    {
        GPIO_ResetBits(LEDPORT, LED1); //LED灯都为低电平(0)
    }
    else
    {
        GPIO_SetBits(LEDPORT, LED1); //LED灯都为高电平(1)
    }
}

//单次按有锁存
void KEY_show2(void)
{
    if(KEY_scan() == 1)
    {
        GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1 - GPIO_ReadOutputDataBit(LEDPORT, LED1))); //通过读取LED当前输出状态实现取反
        while(!KEY1);//等待按键松开
    }
	//其他按键类似
}

//长按有锁存
void KEY_long(void)
{
    static u8 key_flag = 0;
    if((KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0))
    {
        delay_ms(20);
        if(KEY1 == 0)
        {
            key_flag++;
            if(key_flag == 1)
            {

            }
            if(key_flag == 50) //20*50=1000ms=1s
            {
                GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1 - GPIO_ReadOutputDataBit(LEDPORT, LED1)));

            }
        }
		//其他按键类似
    }
    else
        key_flag = 0;
}

方式2(使用定时器扫描方式,长按短按)

按键部分

/*************************GPIO_init.h*************************/
# define KEY1 GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)	//按键B1(A)
# define KEY2 GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_8)	//按键B2(A)
# define KEY3 GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)	//按键B3(B)
# define KEY4 GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_2)	//按键B4(B)

/*************************GPIO_init.c*************************/
void KEYInit()
{
	GPIO_InitTypeDef GPIO_InitStructure;	//定义结构体
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB,ENABLE);	//使能A,B组外设时钟
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_8;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;	//上拉输入
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;	//上拉输入
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
}
/*|||||||||TIMI初始化区|||||||||||*/
void TIM4Init(unsigned int arr,unsigned int psc)
{
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;	//定义结构体
	NVIC_InitTypeDef NVIC_InitStructure;	//定义结构体
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);	//使能TIM4外设时钟
	
	TIM_TimeBaseInitStructure.TIM_ClockDivision = 0;	//分频因子
	TIM_TimeBaseInitStructure.TIM_CounterMode = 0;	//计数模式,0表示向上计数
	TIM_TimeBaseInitStructure.TIM_Period = arr-1;	//自动重装载值(溢出值)
	TIM_TimeBaseInitStructure.TIM_Prescaler = psc-1;	//预分频系数
	TIM_TimeBaseInit(TIM4,&TIM_TimeBaseInitStructure);
	
	NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;	//选择TIM4中断通道
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;	//使能中断通道
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;	//设定抢占优先级为2
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;	//设定响应优先级为1
	NVIC_Init(&NVIC_InitStructure);
	
	TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE);	//使能TIM3中断
	TIM_Cmd(TIM4,ENABLE);	//使能TIM4
}
/*|||||||||中断服务函数区|||||||||||*/
void TIM4_IRQHandler(void)
{
	if(TIM_GetITStatus(TIM4,TIM_IT_Update)==SET)	//判断是否发生中断
	{
		TIM_ClearITPendingBit(TIM4,TIM_IT_Update);	//清除中断标志位
		KeyScan();
	}
}

/*************************KEY.h*************************/
void KeyDriver(void);
void KeyScan(void);

/*************************KEY.c*************************/
# include "stm32f10x.h"
# include "GPIO_init.h"
# include "LED.h"

unsigned char longkeyFlag = 0;	//长按标志
unsigned int key_count=0;	//计数
unsigned char KeySta[4]={1,1,1,1};	//按键当前状态
unsigned int KeyDownTime[4]={0,0,0,0};	//保存每个按键按下的时间累加

extern void KeyAction(unsigned char keycode,unsigned char keymode);

void KeyDriver(void)
{
	unsigned char i;
	
	static unsigned char backup[4]={1,1,1,1};	//上一次按键的状态
	static unsigned int TimeThr[4]={4000,4000,4000,4000};	//初始值表示按键第一次按下之前等待的时间
	for(i=0;i<4;i++)
	{
		if(KeySta[i]!=backup[i])	//说明按键状态发生改变
		{
			if(longkeyFlag)	//如果执行了长按则恢复原样否则会触发短按
			{
				longkeyFlag = 0;
				backup[i]=1;
				KeySta[i]=1;
			}
			if(backup[i]==0)	//表示按下执行短按
			{
				key_count = 0;	//需要清0不然有BUG:经调试按十多次会触发到长按!
				KeyAction(i+1,0);
			}
			backup[i] = KeySta[i];	//把当前状态值存起来
		}
		//长按
//		if(KeyDownTime[i]>0)	//说明按下
//		{
//			if(KeyDownTime[i]>TimeThr[i])
//			{
//				KeyAction(i+1,longkey);
//				TimeThr[i] = TimeThr[i]+200;	//表示按键按住不放,按键状态翻转需要等待的时间
//			}
//		}
		else	//说明按键弹起
		{
			TimeThr[i] = 1000;
		}
	}
}

//按键扫描,利用定时器2ms扫描一次,8*2=16ms
void KeyScan(void)
{

	unsigned char i;
	static unsigned char keybuf[4]={0xFF,0xFF,0xFF,0xFF};
	keybuf[0] = (keybuf[0]<<1)|KEY1;
	keybuf[1] = (keybuf[1]<<1)|KEY2;
	keybuf[2] = (keybuf[2]<<1)|KEY3;
	keybuf[3] = (keybuf[3]<<1)|KEY4;
	
	for(i=0;i<4;i++)
	{
		if(keybuf[i]==0xFF)	//表示弹起
		{
			KeySta[i] = 1;
			KeyDownTime[i] = 0;
		}
		else if(keybuf[i]==0x00)	//表示按下
		{
			KeySta[i] = 0;
//			KeyDownTime[i] = KeyDownTime[i]+4;
			key_count++;
			if(key_count>=2000)	//表示按下执行长按
			{
				KeyAction(i+1,1);
				key_count = 0;
				longkeyFlag=1;
			}
		}
		else
		{
			//空语句消抖
		}
	}
}

/*************************main.c*************************/
void KeyAction(unsigned char keycode, unsigned char keymode);

int main(void)
{
	KEYInit();	//初始化
	TIM4Init(1000, 72);	//定时1ms	如果是2ms:2000/(72000000/72)=0.002s 
    while(1)
	{
    	KeyDriver();
	}
}

//按键执行内容 参数1:哪个按键	参数2:短按:0长按:1
void KeyAction(unsigned char keycode, unsigned char keymode)
{
    static unsigned char LEDflag = 0;
    if(keycode == 1)	//按键1
    {
        if(keymode == 0)
        {
			//执行对应操作
        }
    }
    else if(keycode == 2)	//按键2
    {
        if(keymode == 0)
        {
            //执行对应操作
        }
    }
    else if(keycode == 3)	//按键3
    {
        if(keymode == 0)
        {
            //执行对应操作
        }
        if(keymode==1)
        {
			//执行对应操作
        }
    }
    else if(keycode == 4)	//按键4
    {
        if(keymode == 0)
        {
			//执行对应操作
        }
    }
}

USART

  • 开发板上有两个串口,一个串口1,一个串口2,比赛一般使用串口2

串口 USART1 USART2 USART3
发送/接收(Tx/Rx) PA9/PA10 PA2/PA3 PB10/PB11
所在总线 APB2 APB1 APB1
  • Lib 文件添加 stm32f10x_usart.c ,misc.c(NVIC的),否则编译会错误

  • 需要重定向 fputc 函数才能使用 printf 函数发送数据(添加头文件 “stdio.h” 并且在设置把选项打钩)

  • 中文数据手册第489

  • 波特率一般是 9600

程序编写

  • RxBuffer[Usart_Index-1] = 0; 当接收到 \n 后,前一个就是 \r,所以直接把前一个数据赋值为 0,覆盖掉即可(如果不是根据判断是否接到’\n’为结束可不加)
  • 一帧数据处理完毕后,别忘了清空缓冲区(memset)
  • 当勾选串口助手的回车换行后,我们判断最后一个字符要写成 if('\n' == temp)而加入我们是人为写入 \n 的话,我们要写成 if('n' == temp)
/*************************GPIO_init.h*************************/
# define USART2_TX GPIO_Pin_2	//发送(A)
# define USART2_RX GPIO_Pin_3	//接收(A)

extern u8 RxBuffer[20];	//USART接收数组
extern u8 Receive_Flag;	//接收完成标志位(1:完成0:未完成)

/*************************GPIO_init.c*************************/
u8 RxBuffer[20] = {0};
u8 Receive_Flag = 1;
u8 Usart_Index = 0;	//接收数组下标

//USART2初始化
void USARTInit(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;	//定义结构体
    USART_InitTypeDef USART_InitStructure;	//定义结构体
    NVIC_InitTypeDef NVIC_InitStructure;	//定义结构体

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);	//使能A组时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);	//使能USART2时钟

    GPIO_InitStructure.GPIO_Pin = USART2_RX;	//接收
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;	//浮空输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin = USART2_TX;	//发送
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    USART_InitStructure.USART_BaudRate = 9600;	//波特率
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;	//数据位
    USART_InitStructure.USART_StopBits = USART_StopBits_1;	//停止位
    USART_InitStructure.USART_Parity = USART_Parity_No;	//无奇偶校验
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;	//禁止硬件流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式
    USART_Init(USART2, &USART_InitStructure);	//初始化
    USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);	//接收中断开启
    USART_Cmd(USART2, ENABLE);	//开启USART2

    NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;	//USART2通道
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;	//使能通道
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;	//抢占优先级为0
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;	//响应优先级为1
    NVIC_Init(&NVIC_InitStructure);
}

//USART2中断
void USART2_IRQHandler(void)
{
    u8 temp;
    if(USART_GetITStatus(USART2, USART_IT_RXNE) == SET)	//判断是否发生中断
    {
        USART_ClearITPendingBit(USART2,USART_IT_RXNE);	//清除中断标志位
        temp = USART_ReceiveData(USART2);	//接收数据
        if(('\n'==temp) || (20==Usart_Index))	//接收到\n或者超过数组大小则完成接收
        {
            Receive_Flag = 1;
            RxBuffer[Usart_Index-1] = 0;
            Usart_Index = 0;
            USART_ITConfig(USART2, USART_IT_RXNE, DISABLE);	//接收完毕关闭中断
        }
        else
        {
            RxBuffer[Usart_Index++] = temp;
        }
    }
}

/*************************USART.h*************************/
# include <stdio.h>

int fputc(int ch, FILE *f);

/*************************USART.c*************************/
//返回值:	成功:返回被写入的字符 错误:返回 EOF
//重定向printf函数
int fputc(int ch, FILE *f)
{
    USART_SendData(USART2, (uint8_t)ch); //发送一个字节数到串口
    while(USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET); //等待发送完成,1:完成,0:还没完成
    return ch;
}
/*************************main.c*************************/
int main(void)
{
	USARTInit();
    while(1)
    {
        window((const char *)arr);	//LCD显示
        KeyDriver();
    } 
}

//在window函数里面调用即可
    if(1 == Receive_Flag)	//发送串口中断
    {
        Receive_Flag = 0;
        LCD_ClearLine(Line1);
        sprintf((char *)str1, "USART2:%s", RxBuffer);
        LCD_DisplayStringLine(Line1, str1);
        memset(RxBuffer, 0, sizeof(RxBuffer));	//清空
        USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);	//打开接收中断
    }

LCD屏

LCD STM32
LCD_CS# PB9
LCD_RS PB8
LCD_WR# PB5
LCD_RD# PB10
H_D0~H_D7 PC8~PC15
  • 比赛会给LCD的例程的,不需要我们自己写,但是还是要修改几处地方,因为LCD 和 LED 之间互相会影响所以要用到锁存器(LED的初始化最好放在LCD后面),修改下面3个函数
/*找到这3个函数
LCD_WriteReg()
LCD_WriteRAM_Prepare()
LCD_WriteRAM()
*/

//函数第一行加上
unsigned short PCOUT=GPIOC->ODR;
//在最后一行加上,用于锁存,否则导致操作LCD时LED不受控制
GPIOC->ODR=PCOUT;
  • LCD 大概内容

  • 显示变量需要用到 “stdio.h” 里面的 sprintf 函数

  • 对液晶每一行来说,液晶像素序号 从右向左编号依次为(左)319,318,317......0(右),指定列像素光标位置后,从当前像素位置开始向右写入字符,然后光标向右移动到一个下一像素位置(注意的是,如果指定的是0,写完0后,移动的下一位是319;如果指定大于319,比如330,依然会从319开始向右写

  • 液晶显示屏 长320,宽240,共划分为 10 行,编号1–10(Line0~Line9),每行大小 320*24 像素

  • 每个ASCII字符占用 16*24 个像素位置,一行最多显示 20个字符(即20列), LCD_DisplayChar 函数可以指定某列显示一个字符

//在第i列显示单个字符计算公式:319-16*(i-1)
u8 str[20];
u16 n = 1024;
float p = 3.1415;
STM3210B_LCD_Init();
LCD_Clear(Yellow);//别忘了这一句总体清除
LCD_SetBackColor(Blue);
sprintf((char *)str, "num:%d pi:%.2f", n, p);
LCD_DisplayStringLine(Line1, str);
LCD_SetBackColor(Red);
LCD_DisplayChar(Line1, 319 - 16 * 19, 'X');

  • LCD_DrawRect(x,y,h,w); 函数是画一个矩形
  • x 是控制上下,数值越大越往上;y是控制左右,数值越大越往左(上大下小左大右小暂时没深入这个
  • LCD_DrawRect(70,210,100,100); —矩形显示中间

按键控制num值递增

LCD高亮单个字符

  • 第一个参数是 需要显示的字符串
  • 第二个参数是 需要高亮的字符坐标(0~19,只有20列),左到右

main.c

char str2[20]="0123456789abcdefghij";//注意用char类型

void highlight(char *str,uint8_t pos)
{
	int i = 0;
	u8 num=0;
	num=strlen(str);//计算有多少个
	for(i = 0; i <num; i++)//最后一个字符是乱码所以去掉
	{
		if(i != pos)
			LCD_DisplayChar(Line4,(320 - (16 * i)),str[i]);
	}
	LCD_SetBackColor(Yellow);//需要高亮的背景颜色
	LCD_DisplayChar(Line4,(320 - (16 * pos)),str[pos]);//需要高亮的地方
	LCD_SetBackColor(White);	//这句要加上否则全部都变成高亮
}

  • 字体颜色(跟上面一样只把 back 改成 Text 即可)

LCD字串符右对齐显示

main.c

u8 lcd_str[20];
u8 lcd_temp[20];

memset(lcd_str, 0, sizeof(lcd_str));//清空数组
sprintf((char *)lcd_temp, "Total(km):%d", 12345678);
sprintf((char *)lcd_str, "%20s", lcd_temp); //左减右加,20列所以这里是20
LCD_DisplayStringLine(Line9 , lcd_str);

LCD补空格

  • LCD_DisplayStringLine 函数在打印一段char数组时,全部打完或者超过20个(一行最多20个字符)时,就会停下来,比如打了15个下次再在这行后面则第一次打印的后3个字符就不会刷新,所以一般选择空格去盖,所以最好补齐20个字符,新建一个函数把自带的函数内容复制过来修改即可
void LCD_DisplayStringLine2(u8 Line, u8 *ptr)
{
    unsigned long i = 0;
    unsigned int refcolumn = 319;
    char a = ' ';
    while(i<20)
    {
        if(0==*ptr)
        {
            LCD_DisplayChar(Line,refcolumn,a);
        }
        else
        {
            LCD_DisplayChar(Line,refcolumn,*ptr);
            ptr++;
        }
        refcolumn -= 16;
        i++;
    }
}

程序编写

/*************************main.c*************************/
unsigned char arr[20]="\0";	//数据存放的数组
unsigned char HightLight = 0;	//选择是否高亮(0:不高亮1:高亮)

int main(void)
{
    STM3210B_LCD_Init();
	delay_ms(50);	//延时50ms
	LCD_Clear(White);
	LCD_SetTextColor(Black);
	LCD_SetBackColor(White);
    
    while(1)
    {
        window((const char *)arr);	//需要强制类型转换
        KeyDriver();
    }
}

//按键执行内容 参数1:哪个按键	参数2:短按:0长按:1
void KeyAction(unsigned char keycode, unsigned char keymode)
{
    if(keycode == 1)
    {
        if(keymode == 0)
        {
            strcpy((char*)arr,"HELLO");
			LED_display_choose(LEDALL,0);	//灭所有灯
			LED_display_choose(LED1,1);	//点亮LED1
        }
    }
    else if(keycode == 2)
    {
        if(keymode == 0)
        {
			strcpy((char*)arr,"NO");
			LED_display_choose(LEDALL,0);	//灭所有灯
            LED_display_choose(0xFFFF^LED1,1);	//点亮除了LED1以外的所有LED
        }
    }
    else if(keycode == 3)
    {
        if(keymode == 0)
        {
		   	strcpy((char*)arr,"YES");
			LED_display_choose(LEDALL,0);	//灭所有灯
        }
        if(keymode==1)
        {
			Buzz_display();	//蜂鸣器响一下
        }
    }
    else if(keycode == 4)
    {
        if(keymode == 0)
        {
		   HightLight=!HightLight;	//是否高亮	  
        }
    }
}

//LCD显示
void window(const char* arr)	//注意数据类型
{
	unsigned char str[20];
	static unsigned char LastLen = 0;	//保存上一次的数组长度
	unsigned char i;
	unsigned char StaLen = strlen(arr);	//计算当前数组长度
	if(StaLen<LastLen)	//当当前长度小于上一次则需要空格盖住
	{
		for(i=StaLen;i<=LastLen;i++)
		{
			str[i]=' ';
			LCD_DisplayChar(Line0,320-(StaLen*16),str[i]);
			StaLen+=1;
		}
	}
	if(1==HightLight)
	{
		LCD_SetBackColor(Red);
	}
	else if(0==HightLight)
	{
		LCD_SetBackColor(White);
	}
	LastLen=sprintf((char*)str,"%s",arr);
	LCD_DisplayStringLine(Line0,str);
	LCD_SetBackColor(White);
}

ADC

ADC相关通道

  • 不同ADC应用不同通道时,可以同时进行采样和转换, 但不可以对相同通道同时采样
通道 ADC1 ADC2 ADC3
通道0 PA0 PA0 PA0
通道1 PA1 PA1 PA1
通道2 PA2 PA2 PA2
通道3 PA3 PA3 PA3
通道4 PA4 PA4 PF6
通道5 PA5 PA5 PF7
通道6 PA6 PA6 PF8
通道7 PA7 PA7 PF9
通道8 PB0 PB0 PF10
通道9 PB1 PB1 连接内部VSS
通道10 PC0 PC0 PC0
通道11 PC1 PC1 PC1
通道12 PC2 PC2 PC2
通道13 PC3 PC3 PC3
通道14 PC4 PC4 连接内部VSS
通道15 PC5 PC5 连接内部VSS
通道16 连接内部温度传感器 连接内部VSS 连接内部VSS
通道17 连接内部Vrefint 连接内部VSS 连接内部VSS
  • ​ 在使用ADC 外部通道时,可以设定为规则组和注入组规则组就是设定好转换顺序后,按照规则正常转换注入组类似于中断,可以插队,当触发信号触发注入组通道时,优先转换注入组,转换完后再继续转换规则组;如果正在转换规则通道期间,注入通道被触发,当前规则组转换被复位,注入通道序列被以单次扫描方式转换,完成转换后恢复上次被中断的规则通道转换。(蓝桥板子也没有用到多么复杂,我们就直接使用规则组就好了。)
  • 当“转换组”只有一个通道转换时称之为 单通道模式,当有多个通道按顺序转换时称之为 多通道模式或者扫描模式(蓝桥杯使用单通道即可)
  • 当规则组或注入组的通道按照设定顺序执行一次采转换后即停止工作,这种模式称之为 单次转换模式;如果执行完一次转换后,ADC 没有停止,而是立即启动新一轮转换,这种模式称之为 连续转换模式(蓝桥板子,使用单次转换即可)
  • 触发启动转换:触发启动转换分为两种方式,分别是 软件触发和外部事件触发(外部是相对于 ADC 外设来讲),其中外部事件触发又分为 定时器触发和外部触发(这里的外部指的是芯片外部信号)(蓝桥板子,使用软件触发即可)
  • 双ADC 模式 是 ADC1 和 ADC2 同时采集某些参数,比如要获取瞬时功率需要同时采集电压和电流参数才能准确计算结果,这种场合就必须使用双ADC模式
  • 当使用多通道转换时, 必须在后一个通道转换完毕之前就把数据取走(一般使用DMA 传输模式)
  • 建议在每次上电时执行一次 ADC 校准, 启动校准前,必须先使能ADC,至少超过两个 ADC 时钟周期
  • 切记获取到的ADC值,进行 * 3.3 / 4096 进行量化, 并且最后的值存入一个 float 类型中

板子上的ADC

ADC STM32
M_PB0 PB0
  • 手册19页可以知道PB0引脚复用
默认 复用
PB0 ADC_IN8,TIM3_CH3
  • STM32F103RBT6的ADC时钟及转换时间

蓝桥板载STM32F103RBT6拥有 2路12位 (0~4096)ADC,ADC挂载在 APB2总线 上,且ADC 最大时钟不超过14MHz。所以当APB2总线设置为72M,有必要对 其分频再用于ADC

ADC的转换时间 = 采样时间 + 12.5周期(采样时间和实际电路有着莫大的关系,但是对于蓝桥的板子而言,对于转换速度没有太大要求我们一般设置为:ADC_SampleTime_239Cycles5239.5个周期即可。)

  • DMA初始化在前,ADC初始化在后(这个不能调乱!!!)
  • 可以用平均值滤波,也可以用卡尔曼滤波(推荐这个!)
  • 关于测量结果误差,一般控制在5%以内即可(考虑到硬件稳定问题)
/*************************ADC.h*************************/
# define D_num 2	//通道数

void KalmanFilter(void);

/*************************ADC.c*************************/
# include "GPIO_init.h"

//卡尔曼滤波(采集2个通道,如果单通道可把数组改成单个变量)
void KalmanFilter(void)
{
    u8 i,j;
    float vdata[D_num];
    //测试时通过q从大往小调,r从小往大调,寻找合适的参数
    static float LastData[D_num] = {0,0}; //上一个数据
    static float Reckon[D_num] = {2.50,2.50};	//当前估算值
    static float Q = 0.0033,R = 0.2;	//Q控制误差 R控制响应速度(越大响应速度越慢)
    static float kGain[D_num] = {0,0}; //kGain增益

    for(j=0; j<D_num; j++)
    {
        vdata[j] = padc.ADC_DMA_Data[j];
    }
    for(i=0; i<D_num; i++)
    {
        Reckon[i] += Q;
        kGain[i] = Reckon[i] / ( Reckon[i] + R ); //计算卡尔曼增益
        vdata[i] = LastData[i] + ( kGain[i] * ( vdata[i] - LastData[i] ) ); //计算本次滤波估计值
        Reckon[i] = ( 1 - kGain[i] ) * Reckon[i]; //更新测量方差
        LastData[i] = vdata[i];
    }
    padc.v1 = vdata[0];
    padc.v2 = vdata[1];
}

/*************************GPIO_init.h*************************/
# define ADC_1 GPIO_Pin_0	//通道8(B)
# define ADC_2 GPIO_Pin_4	//通道4(A)

typedef struct ADC_class
{
    u16 ADC_DMA_Data[2];	//采集2个通道
    float v1;	//转换后电压1
    float v2;	//转换后电压2
} AD1;
extern AD1 padc;

/*************************GPIO_init.c*************************/
AD1 padc;

//DMA初始化
void DMA1Init(void)
{
    DMA_InitTypeDef DMA_InitStructure;	//定义结构体

    DMA_DeInit(DMA1_Channel1);	//复位DMA通道1
    DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&ADC1->DR;	//ADC1外设基地址
    DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&padc.ADC_DMA_Data;	//DMA通道数据存储器
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;	//外设为数据来源
    DMA_InitStructure.DMA_BufferSize = 2;	//数据大小
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;	//外设地址不递增
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;	//内存地址递增
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;	//半字
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;	//半字
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;	//循环模式
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;	//优先级高
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;	//禁用内存-->内存
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);
    DMA_Cmd(DMA1_Channel1, ENABLE);	//使能通道1
}

//ADC初始化
void ADC1Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;	//定义结构体
    ADC_InitTypeDef ADC_InitStructure;	//定义结构体

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOA,ENABLE);	//开启ADC1,B组外设时钟
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);	//使能DMA1


    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;	//模拟输入
    GPIO_InitStructure.GPIO_Pin = ADC_1;	//PB^0
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB,&GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;	//模拟输入
    GPIO_InitStructure.GPIO_Pin = ADC_2;	//PA^5
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);

    DMA1Init();	//DMA初始化在前

    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;	//独立模式
    ADC_InitStructure.ADC_ScanConvMode = ENABLE;	//使能多通道
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;	//使能连续模式
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//软件控制转换
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;	//右对齐
    ADC_InitStructure.ADC_NbrOfChannel = 2;	//通道数
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 1, ADC_SampleTime_239Cycles5);	//通道8,239.5周期
    ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 2, ADC_SampleTime_239Cycles5);
    ADC_DMACmd(ADC1, ENABLE);	//开启ADC的DMA支持
    ADC_Cmd(ADC1, ENABLE);	//使能ADC1
    ADC_ResetCalibration(ADC1);	//重置指定的ADC的校准寄存器
    while(ADC_GetResetCalibrationStatus(ADC1));	//获取ADC重置校准寄存器的状态
    ADC_StartCalibration(ADC1);	//开始指定ADC的校准状态
    while(ADC_GetCalibrationStatus(ADC1));	//获取ADC重置校准寄存器的状态
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);	//使能ADC1软件开始转换
}

/*************************main.c*************************/
int main(void)
{
    ADC1Init();
}

//在window函数调用即可
void window(const char *arr)
{
    u8 str4[20];	//Line4
//   省略中间无关紧要部分....

    KalmanFilter();	//调用卡尔曼滤波
    sprintf((char *)str4, "C8:%.2fV C4:%.2fV",padc.v1* 3.3 / 4095,padc.v2* 3.3 / 4095);
    LCD_DisplayStringLine(Line4, str4);
}

RTC实时时钟

  • RTC 的时钟源—— RTCCLK

当系统主电源关闭时,HSE 无法工作,而如果采用LSI 作为RTC 时钟源,一方面精度相对较低,另外一方面会有相对大的功率消耗, 所以大多数情况下RTC 的时钟源是采用LSE,LSE 的晶振的负载电容要求为 6pF

  • RTC 核心部分是一个 32 位 可编程、向上计数 的计数器

  • 需要注意的是,对 RTC 任何寄存器的 操作, 都必须在前一次写操作结束后才能继续进行。

  • 首先先确保 Lib 文件下有这些文件,没有就添加进来

  • 蓝桥杯嵌入式板子,只能使用 LSI 时钟源,据官方说明是用不了其他时钟源的,所以这点是非常重要的!

  • 设置初始时间(这个时间是按照BCD码来存储的,可以简单的理解成,是按秒数来储存的,因此小时,分,都得化为秒,相加在一起)

  • 设置RTC分频系数 ,为什么要选择40000,因为我们的 RTC 选择的是 LSI 作为时钟源,我们首先得看一下 LSI 的时钟频率

/*************************GPIO_init.h*************************/
extern u8 RTC_Flag;	//RTC中断标志

/*************************GPIO_init.c*************************/
u8 RTC_Flag = 1;
//RTC初始化
void RTCInit(u32 Time)
{
    NVIC_InitTypeDef NVIC_InitStructure;	//定义结构体

    NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);	//使能PWR,BKP外设时钟
    PWR_BackupAccessCmd(ENABLE);	//后备域解锁
    BKP_DeInit();	//后备域复位
    RCC_LSICmd(ENABLE);	//使能LSI时钟
    while(RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);	//等待变1
    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);	//RTC时钟源配置成LSI
    RCC_RTCCLKCmd(ENABLE);	//开启RTC
    RTC_WaitForSynchro();	//等待时钟同步
    RTC_WaitForLastTask();	//等待对RTC寄存器操作完成
    RTC_ITConfig(RTC_IT_SEC, ENABLE);	//使能秒中断
    RTC_WaitForLastTask();	//等待对RTC寄存器操作完成
    RTC_SetPrescaler(40000);	//设置RTC周期为1秒
    RTC_WaitForLastTask();	//等待对RTC寄存器操作完成
    RTC_SetCounter(Time);	//写入时间
    RTC_WaitForLastTask();	//等待对RTC寄存器操作完成
}

//RTC中断
void RTC_IRQHandler(void)
{
    u32 Times;
    if(RTC_GetITStatus(RTC_IT_SEC) == SET)
    {
        RTC_ClearITPendingBit(RTC_IT_SEC);//清除标志位
        RTC_Flag = 1;

        Times = RTC_GetCounter(); //获取时间
        RTC_WaitForLastTask();
        if(Times == (24 * 3600-1)) //这样就不会导致00:00:00后卡顿2s
        {
            RTC_SetCounter(0);//写入0
        }
    }
}

/*************************main.c*************************/
int main(void)
{
    RTCInit(23*3600+59*60+50);
}

//在window函数调用
void window(const char *arr)
{
    u8 str3[20];	//Line3

    u8 hour,min,sec;	//小时,分,秒
    u32 TimeVal;	//存储获取的时间
    
    if(1 == RTC_Flag)	//发生RTC中断
    {
        RTC_Flag = 0;
        TimeVal = RTC_GetCounter();	//获取时间(获取的是总秒数)
        hour = TimeVal / 3600;
        min = TimeVal % 3600 / 60;
        sec = TimeVal % 3600 % 60;
        sprintf((char *)str3, "Time:%.2d:%.2d:%.2d  ", hour, min, sec);
        LCD_DisplayStringLine(Line3, str3);
    }
}

PWM(超级重点)

  • 蓝桥杯主要考的定时器也就三个 TIM1,TIM2,TIM3(TIM1为高级定时器用来产生互补PWM波,TIM2,TIM3是普通定时器拿来捕获/比较通道、输入捕获、PWM模式)
  • 每个通用定时器有4路捕获/比较通道,可产生四路PWM波
  • CT117E开发板按键旁有一串引出来的IO口,分别是PA1~PA7,它们是可以复用为定时器通道的
TIM通道 STM32 扩展板说明
TIM2_CH2 PA1 可以调节输入PWM的频率
TIM2_CH3 PA2 可以调节输入PWM的频率
TIM2_CH4 PA3 /
TIM3_CH1 PA6 可以调节输入PWM的占空比
TIM3_CH2 PA7 可以调节输入PWM的占空比
  • 一般会考这些

1、一路或两路 特定频率特定占空比占空比可调 PWM(PWM输出即可)
2、同一定时器输出 两路频率不同单独可调占空比恒定可调(PWM输出比较)
3、 一路PWM捕获(PWM捕获即可)
4、 两路PWM捕获(输入捕获)

5、PWM产生互补波形/死区(可能国赛有)

PWM模式

PWM模式: 同一定时器中,不同的通道下,输出的频率固定,占空比可变(即假如我们通道1输出1KHz方波,那么通道2同样也是1KHz方波)

  • 开始编写代码,利用 TIM2 两个通道产生 PWM
  • 需要注意 TIM2 的通道CH3 会影响 USART2_TX引脚,共用同个管脚而且这款芯片没有重映射功能(如果两个同时使用的话,USART发送会变乱码)
  • 高级定时器必须有这一句,而通用就不必了 TIM_CtrlPWMOutputs(TIM3, ENABLE);//使能PWM输出

/*************************GPIO_init.c*************************/
//PWM模式(频率固定,占空比可调)定时器2
void PWM_1Init(u16 arr,u16 psc)
{
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;	//定义结构体
	TIM_OCInitTypeDef  TIM_OCInitStructure;	//定义结构体
	GPIO_InitTypeDef GPIO_InitStructure;	//定义结构体
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2;	//PA1,PA2(通道2,3)
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	TIM_TimeBaseStructure.TIM_Period = arr - 1;	//溢出值
	TIM_TimeBaseStructure.TIM_Prescaler = psc - 1;	//预分频
	TIM_TimeBaseStructure.TIM_ClockDivision = 0;
	TIM_TimeBaseStructure.TIM_CounterMode = 0;
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
	//通道2
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;	//PWM模式1
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;	//比较输出使能
	TIM_OCInitStructure.TIM_Pulse = 0;	//CRR值
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;	//极性:高
	TIM_OC2Init(TIM2, &TIM_OCInitStructure);
	TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable);	//使能TIM2在CCR上的预装载寄存器
	//通道3
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;	//PWM模式1
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
	TIM_OCInitStructure.TIM_Pulse = 0;	//设0即可
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;	//极性:高
	TIM_OC3Init(TIM2, &TIM_OCInitStructure);
	TIM_OC3PreloadConfig(TIM2, TIM_OCPreload_Enable);
	
	TIM_ARRPreloadConfig(TIM2, ENABLE);
	TIM_Cmd(TIM2, ENABLE);	//使能TIM2
}

/*************************GPIO_init.c*************************/
u16 pad1 = 200,pad2 = 100;	//初值20%占空比,10%占空比

int main(void)
{
    PWM_1Init(1000,72);	//频率=72Mhz/(72*1000)=1Khz=1ms
	TIM_SetCompare2(TIM2,pad1);//通道2,初始值占空比为20%
	TIM_SetCompare3(TIM2,pad2);//通道3,初始值占空比为10% 与USART发送管脚一起用会冲突
}

//通过按键可以控制占空比
void KeyAction(u8 keycode, u8 keymode)
{
    if(keycode == 1)
    {
        if(keymode == 0)
        {
            if(pad1 >= 200 && pad1 < 900)
			{
				pad1+=100;	//按一下加10%,最大到90%
				TIM_SetCompare2(TIM2,pad1);	//不能只修改pad1,还需要重新写入才生效!
			}
			else //加到90%后再加变回20%
			{
				pad1 = 200;
				TIM_SetCompare2(TIM2,pad1);
			} 
        }
    }
}

//window函数显示
void window(const char *arr)
{
    u8 str5[20];	//Line5

    sprintf((char*)str5,"Duty1:%d%% Duty2:%d%%",pad1/10,pad2/10);	//%需要转义
    LCD_DisplayStringLine2(Line5, str5);
}

PWM输出比较

PWM比较模式: 同一定时器下,不同通道能够产生频率不同,占空比不同,甚至相位也不同的方波

输出比较:定时器的计数值一直向上计数的同时,他要不断的跟我们设定的 pulseDuty做比较

​ 什么是 PWM_OCTOGGLE模式?在定时器相关即寄存器中,有两个捕获/比较模式寄存器(TIMx_CCMR1、TIMx_CCMR2),设置成 PWM_OCTOGGLE 模式后(也叫翻转模式),当 TIMx_CCRx=TIMx_CNT时,翻转OC1REF的电平

程序编写

  • 注意CH2_Duty的定义范围是 u32类型(值是占空比所计数的值,不是占空比值)

  • 计数值初始化设置成 65535

  • 引脚产生PWM波,因此我们要配置为 复用推挽输出模式

  • 因为是 71 分频,所以pulse的值就等于 1000000 / 输入的频率值

  • 通过设定的占空比数,算出 Duty 的位置

  • pulse 就相当于我们设定的周期值,在一个pulse内,小于Duty为高电平,大于Duty为低电平

  • ​ 在每次进入比较中断时,我们都把通道当前的比较值获取到 capture 里,当我们设定的 Flag为真 时,代表将进入 Duty以下 的位置,Flag为假 则代表将进入 Duty以上 的位置。 当我们每次得到一个中断,引脚都会自动的把电平翻转,我们在中断处理函数中 只需要设定好比较值

/*************************GPIO_init.h*************************/
typedef struct PWM_2
{
	u32 CH1_Val;	//周期对应计数值
	u32 CH2_Val;	
	u32 CH1_Duty;	//占空比对应的计数量
	u32 CH2_Duty;	
	u32 fre1;	//频率
	u32 fre2;
	u8 duty1;	//占空比
	u8 duty2;
	
}PWM_compare;
extern PWM_compare P1;

/*************************GPIO_init.c*************************/
PWM_compare P1;

//PWM比较(频率可调,占空比可调)定时器3
void PWM_2Init(u16 fre1,u8 duty1,u16 fre2,u8 duty2)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    NVIC_InitTypeDef NVIC_InitStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;	//定义结构体

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO,ENABLE);	//使能A组,复用时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;	//PA6,PA7
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);

    TIM_TimeBaseStructure.TIM_ClockDivision = 0;	//分频因子
    TIM_TimeBaseStructure.TIM_CounterMode = 0;	//向上计数
    TIM_TimeBaseStructure.TIM_Period = 65535;	//溢出值
    TIM_TimeBaseStructure.TIM_Prescaler = 71;	//预分频
    TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure);

    NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
	
	P1.fre1 = fre1;
	P1.fre2 = fre2;
	P1.duty1 = duty1;
	P1.duty2 = duty2;
    P1.CH1_Val = 1000000/P1.fre1;	//CCR1值
    P1.CH2_Val = 1000000/P1.fre2;	//CCR2值
    P1.CH1_Duty = P1.CH1_Val*P1.duty1/100;	//占空比所占计数值
    P1.CH2_Duty = P1.CH2_Val*P1.duty2/100;	//占空比所占计数值

    //通道1
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_Toggle;	//比较模式
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;	//输出比较使能
    TIM_OCInitStructure.TIM_Pulse = P1.CH1_Val;	//比较值(决定占空比)
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;	//低电平有效
    TIM_OC1Init(TIM3, &TIM_OCInitStructure);

    //通道2
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_Toggle;	//比较模式
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = P1.CH2_Val;
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;	//低电平有效
    TIM_OC2Init(TIM3, &TIM_OCInitStructure);

	//不清0第一次波形会有错误
    TIM_SetCounter(TIM3, 0);//定时器计数值清0
    TIM_SetCompare1(TIM3, 0);//定时器捕获比较1寄存器值清0
    TIM_SetCompare2(TIM3, 0);//定时器捕获比较2寄存器值清0

    TIM_Cmd(TIM3, ENABLE);
    TIM_ITConfig(TIM3, TIM_IT_CC1 | TIM_IT_CC2, ENABLE);
}

//TIM3中断
void TIM3_IRQHandler(void)
{
    u16 capture = 0;	//当前比较值
    static u8 flag1,flag2 = 0;
    if (TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET)
    {
        TIM_ClearITPendingBit(TIM3, TIM_IT_CC1 );
        capture = TIM_GetCapture1(TIM3);	//获取通道1的当前的比较值
        if(flag1)
        {
            TIM_SetCompare1(TIM3, capture + (1000000/P1.fre1*P1.duty1/100) );	//设置通道1的值为:当前比较值+高电平
            //printf("1:%d -- %d\r\n",capture,P1.CH1_Val*P1.duty1/100);
        }
        else
        {
            TIM_SetCompare1(TIM3, capture + (1000000/P1.fre1) - (1000000/P1.fre1*P1.duty1/100) );	//设置通道1的值为:当前比较值+低电平
            //printf("0:%d -- %d\r\n",capture,P1.CH1_Val - (P1.CH1_Val*P1.duty1/100));
        }
        flag1 ^= 1;
    }

    if (TIM_GetITStatus(TIM3, TIM_IT_CC2) != RESET)
    {
        TIM_ClearITPendingBit(TIM3, TIM_IT_CC2);
        capture = TIM_GetCapture2(TIM3);	//获取通道2的当前的比较值
        if(flag2)
        {
            TIM_SetCompare2(TIM3, capture + (1000000/P1.fre2*P1.duty2/100));	//设置通道2的值为:当前比较值+高电平
        }
        else
        {
            TIM_SetCompare2(TIM3,capture + (1000000/P1.fre2) - (1000000/P1.fre2*P1.duty2/100));	//设置通道2的值为:当前比较值+低电平
        }
        flag2 ^= 1;
    }
}

/*************************main.c*************************/
int main(void)
{
  PWM_2Init(2000, 50, 4000, 90);	//通道1:0.5ms 50%占空比;通道2:0.25ms 90%占空比  
}
 
//按键里调整占空比/频率
//如果需要修改频率,可修改P1.fre1/P1.fre2的值;修改占空比可修改P1.duty1/P1.duty2的值
void KeyAction(u8 keycode, u8 keymode)
{
    if(keycode == 1)
    {
        if(keymode == 0)
        {
			P1.duty1 += 1;	//通道1:每次加1%占空比
            P1.fre2 += 100;	//通道2:每次加100Hz频率
        }
    }    
}

PWM捕获

本次实验由于输出比较使用了 TIM3 定时器所以这里用 TIM2 定时器,方便获得捕获的数据(把TIM3通道管脚用杜邦线连接TIM2管脚即可捕获)

PWM捕获:上一个实验是比较输出不同频率或者占空比,这个是捕获就是反过来已知波形图而去计算对应占空比和频率

PWM硬件捕获频率(只适用于CH1/CH2)

  • 频率公式:Frequence = 1000000 / (float)(Fre_VAL + 1);
  • 数据手册 13.2 图(直连/非直连)

函数 连接方式 捕获到
TIM_GetCapture2() 直连 周期
TIM_GetCapture1() 直连 占空比
TIM_GetCapture2() 非直连 占空比
TIM_GetCapture1() 非直连 周期
/*************************GPIO_init.c*************************/
//初始化,使用TIM3
void PWM_1Init()
{
	GPIO_InitTypeDef GPIO_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	TIM_ICInitTypeDef  TIM_ICInitStructure;
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;	//CH2->PA7
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;	//上拉输入
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

	NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 4;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

	TIM_TimeBaseStructure.TIM_Period = 65535;
    TIM_TimeBaseStructure.TIM_Prescaler = 71;
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

	TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;	//上升沿
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;//通道二直连
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;	//不分频
    TIM_ICInitStructure.TIM_ICFilter = 0x0;	//不滤波
	TIM_PWMIConfig(TIM3, &TIM_ICInitStructure);	//注意是这个!
	//下面3个不能漏
	TIM_SelectInputTrigger(TIM3, TIM_TS_TI2FP2);
    TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
    TIM_SelectMasterSlaveMode(TIM3, TIM_MasterSlaveMode_Enable);

	TIM_Cmd(TIM3, ENABLE);
	TIM_ITConfig(TIM3, TIM_IT_CC2, ENABLE);
}

//中断服务
u32 Capture1_value=0;
u32 Frequence=0;
void TIM3_IRQHandler(void)
{ 
	if(TIM_GetITStatus(TIM3, TIM_IT_CC2) == SET) 
	{
    TIM_ClearITPendingBit(TIM3, TIM_IT_CC2);
    Capture1_value=TIM_GetCapture2(TIM3);//周期
	if(Capture2_value!=0)
	{
		 Frequence=1000000/(float)(Capture1_value+1);
	}
	else
	{
		 Frequence=0;
	}
  }	 
}

/*************************main.c*************************/
int main(void)
{
    PWM_1Init();
}
//window函数里显示即可

PWM软件捕获(频率/占空比)

  • 有点小bug(两个通道捕获的话有一个通道频率会多1,占空比少1,不知道是不是硬件影响)
  • 下面程序捕获PA6,PA7的PWM,用杜邦线把PA1–>PA6,PA2–>PA7 连接(PA1PA2可以设置成PWM模式产生PWM波形即可)
  • 两个通道不能同时进行捕获,首先两者同时捕获,同时记录这个无所谓,关键是它们还都涉及计算。计算最怕被中断打断,这样可能导致结果不正确。这里目前有两种解决方案,一种是分时复用(当前例程),另一种是关总中断(待实现)
  • 捕获的流程图如下:

  • 定时器中断函数思想

① 当 TIM3_CH1_CAPTURE_MODE=0 时,如果发生了中断,那么就代表我们捕获到了上升沿,当前在上面简图的A点,我们把定时器计数值清0,让定时器重新计数,同时还要把触发方式设置为下降沿触发。

② 当 TIM3_CH1_CAPTURE_MODE=1 时候,发生了中断,那么就代表我们捕获到了下降沿,当前的位置在上面简图的B点,我们获取一次定时器的计数值,同时还得把我们触发方式设置为上升沿触发。

③ 当 TIM3_CH1_CAPTURE_MODE=2 时候,发生中断,那么就代表我们一个周期已经捕获完毕了,当前位置在上面简图的C点,我们再获取一次定时器的计数值,随后就可以根据我们两次获取的计数值,算出我们方波的频率和周期。

④ 当 TIM3_CH1_CAPTURE_MODE=3 时,就意味着我们可以开始计算周期和频率,当我们处理好数据后,我们就可以把TIM3_CH1_CAPTURE_MODE清0,准备开始下一次捕获。

⑤ 然后在主函数中,进行TIM3_CH1_CAPTURE_MODE状态判断,进而计算出占空比和频率的值,然后显示到LCD上,随后再开启下一次捕获。

公式
周期 = 1000000 / 第二次捕获值
占空比 = 第一次捕获值 *100/ 第二次捕获值
/*************************GPIO_init.c*************************/
u8 CAPTURE_MODE = 0;	//分时执行,多通道时需要

//初始化
void PWM_3Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;	//对应结构体
    NVIC_InitTypeDef NVIC_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_ICInitTypeDef TIM_ICInitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);	//使能A组时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);	//使能TIM3

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;	//浮空输入或者上拉输入都行
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;	//CH1->PA6  CH2->PA7
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);

    NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;	//TIM3中断
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 4;
    NVIC_Init(&NVIC_InitStructure);

    TIM_TimeBaseStructure.TIM_Period = 65535;
    TIM_TimeBaseStructure.TIM_Prescaler = 71;
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;	//通道1
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;	//上升沿
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;	//直连
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStructure.TIM_ICFilter = 0x0;
    TIM_ICInit(TIM3, &TIM_ICInitStructure);

    TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;	//通道2
	TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;	//上升沿
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;	//直连
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStructure.TIM_ICFilter = 0x0;
    TIM_ICInit(TIM3, &TIM_ICInitStructure);


    TIM_Cmd(TIM3, ENABLE);
    TIM_ITConfig(TIM3,TIM_IT_CC1|TIM_IT_CC2, ENABLE);
}

/*通道1*/
u8 TIM3_CH1_CAPTURE_MODE = 0;	//当前在哪个位置(一共3个位置:0表示第一个上升沿 1:下降沿 2:第二个上升沿)
u32 TIM3_CH1_CAPTURE_H = 0;	//用来记录第一次捕获的时间
u32 TIM3_CH1_CAPTURE_HL = 0;	//用来记录第二次捕获的时间

/*通道2*/
u8 TIM3_CH2_CAPTURE_MODE = 0;
u32 TIM3_CH2_CAPTURE_H = 0;
u32 TIM3_CH2_CAPTURE_HL = 0;


//TIM3中断(PWM硬件2路捕获)
void TIM3_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM3, TIM_IT_CC1) == 1)	//如果发生中断
    {
        TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);	//清除中断标记
        if(CAPTURE_MODE)	//分时运行500ms切换一次
        {
            switch(TIM3_CH1_CAPTURE_MODE)	//默认是0(即表示来到第一个上升沿)
            {
            case 0:	//来到第一次上升沿
                TIM3_CH1_CAPTURE_H = 0;	
                TIM3_CH1_CAPTURE_HL = 0;
                TIM3_CH1_CAPTURE_MODE = 1;
                TIM_SetCounter(TIM3, 0);//计数值清0开始计数
                TIM_OC1PolarityConfig(TIM3, TIM_OCPolarity_Low);//设置为下降沿触发
                break;
            case 1:	//来到下降沿
                TIM3_CH1_CAPTURE_H = TIM_GetCounter(TIM3);//第一次获取计数值
                TIM3_CH1_CAPTURE_MODE = 2;
                TIM_OC1PolarityConfig(TIM3, TIM_OCPolarity_High);//设置为上升沿触发
                break;
            case 2://来到第二次上升沿
                TIM3_CH1_CAPTURE_HL = TIM_GetCounter(TIM3);//第二次获取计数值
                TIM3_CH1_CAPTURE_MODE = 3;
                TIM_OC1PolarityConfig( TIM3,TIM_OCPolarity_High);//设置为上升沿触发
                break;
            default:
                break;
            }
        }
        else
        {
            TIM3_CH1_CAPTURE_MODE = 0;	//清0然后切换到另一个通道捕获
        }
    }

    if(TIM_GetITStatus(TIM3, TIM_IT_CC2) == 1)
    {
        TIM_ClearITPendingBit(TIM3, TIM_IT_CC2);
        if(!CAPTURE_MODE)
        {
            switch(TIM3_CH2_CAPTURE_MODE)
            {
            case 0:
                TIM3_CH2_CAPTURE_H = 0;
                TIM3_CH2_CAPTURE_HL = 0;
                TIM3_CH2_CAPTURE_MODE = 1;
                TIM_SetCounter(TIM3, 0);
                TIM_OC2PolarityConfig( TIM3,TIM_OCPolarity_Low);
                break;
            case 1:
                TIM3_CH2_CAPTURE_H = TIM_GetCounter(TIM3);
                TIM3_CH2_CAPTURE_MODE = 2;
                TIM_OC2PolarityConfig( TIM3,TIM_OCPolarity_High);
                break;
            case 2:
                TIM3_CH2_CAPTURE_HL = TIM_GetCounter(TIM3);
                TIM3_CH2_CAPTURE_MODE = 3;
                TIM_OC2PolarityConfig( TIM3,TIM_OCPolarity_High);
                break;
            default:
                break;
            }
			
        }
        else
        {
            TIM3_CH2_CAPTURE_MODE = 0;
        }
    }

}

//定时器扫描分时执行通道的捕获
//定时器4中断
void TIM4_IRQHandler(void)
{
	static u16 flag500ms = 0;
	
    if(TIM_GetITStatus(TIM4,TIM_IT_Update) == SET)	//判断是否发生中断
    {
        TIM_ClearITPendingBit(TIM4,TIM_IT_Update);	//清除中断标志位
        KeyScan();
		flag500ms++;
		if(flag500ms >= 500)
		{
			flag500ms = 0;
			CAPTURE_MODE ^= 1;
		}
    }
}

/*************************main.c*************************/
extern u8 TIM3_CH1_CAPTURE_MODE;
extern u32 TIM3_CH1_CAPTURE_H;
extern u32 TIM3_CH1_CAPTURE_HL;

extern u8 TIM3_CH2_CAPTURE_MODE;
extern u32 TIM3_CH2_CAPTURE_H;
extern u32 TIM3_CH2_CAPTURE_HL;
int main(void)
{
    TIM4Init(1000, 72);	//定时1ms	1000/(72000000/72)=0.001s=1ms=1000Hz
    PWM_3Init();
}

//window函数里显示
void window(const char *arr)
{
    u8 str7[20];	//Line7
    u8 str8[20];	//Line8  
    if(TIM3_CH1_CAPTURE_MODE == 3)	//3表示已经捕获到一个周期了
    {
        sprintf((char*)str7,"fre1:%d duty1:%d",1000000 / TIM3_CH1_CAPTURE_HL,TIM3_CH1_CAPTURE_H * 100/TIM3_CH1_CAPTURE_HL);
        LCD_DisplayStringLine(Line7, str7);
        TIM3_CH1_CAPTURE_MODE = 0;	//清0留给下次捕获准备
    }

    if(TIM3_CH2_CAPTURE_MODE == 3)
    {
        sprintf((char*)str8,"fre2:%d duty2:%d",1000000 / TIM3_CH2_CAPTURE_HL,TIM3_CH2_CAPTURE_H * 100/TIM3_CH2_CAPTURE_HL);
        LCD_DisplayStringLine(Line8, str8);
        TIM3_CH2_CAPTURE_MODE = 0;
    }    
}

互补PWM输出

  • 互补PWM输出模式配置和普通PWM输出配置相似,只是在其基础上增加对互补通道的配置。
  • 只有高级定时器才能产生互补(TIM1和TIM8),蓝桥杯板子只有TIM1
  • N表示是互补通道,下面意思是TIM1_CH2N是TIM1_CH2的互补通道
通道 STM32
TIM1_CH2N PB14
TIM1_CH2 PA9

  • sta:避免再次开启时重新配置IO
  • en:题目要求当关闭PWM输出时,两通道必须为低电平。(注意并不是占空比赋值为0就可以的)
/*************************GPIO_init.c*************************/
//PWM互补,定时器1
void PWM_4Init(u16 arr,u16 psc,u8 sta,u8 en)
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;	//定义结构体
    TIM_OCInitTypeDef  TIM_OCInitStructure;	//定义结构体
    GPIO_InitTypeDef GPIO_InitStructure;	//定义结构体

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOA|RCC_APB2Periph_TIM1,ENABLE);	//使能A,B组,TIM1时钟

    if(sta)
    {
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;	//PA9--CH2
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;	//PB14--CH2N
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB,&GPIO_InitStructure);
    }
    TIM_TimeBaseStructure.TIM_Period = arr - 1;	//溢出值
    TIM_TimeBaseStructure.TIM_Prescaler = psc - 1;	//预分频
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = 0;
    TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
    //通道2
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2;	//PWM2模式
    if(en)
    {
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;	//开启OC输出到对应引脚
    TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable;	//互补输出使能。开启OCN输出到对应的管脚   
    }
    TIM_OCInitStructure.TIM_Pulse = 0;
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;	//主通道的输出极性
    TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_Low;	//互补通道的输出极性
    TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Set;	//空闲状态 时输出高电平
    TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCIdleState_Reset;	//互补通道的空闲 时输出低电平
	
	TIM_OC2Init(TIM1,&TIM_OCInitStructure);	//使能
	TIM_Cmd(TIM1, ENABLE);
	TIM_CtrlPWMOutputs(TIM1, ENABLE);	//高级定时器才有  必须打开
}

/*************************main.c*************************/
int main(void)
{
    PWM_4Init(1000,72,1,1);	//1ms=1000Hz
    TIM_SetCompare2(TIM1,400);	//设置占空比40% 因为是PWM2+低电平,所以有效电平是60%
}

蜂鸣器

需要注意:CT117E开发板的蜂鸣器接的引脚是PB4,这个引脚同时也是连接在 JTAG接口 的RST引脚; 当我们stm32复位后, PB4引脚默认是作为JTAG接口的RST引脚,因此我们再使用蜂鸣器之前要先把PB4复用回我们的普通IO口(CT117E嵌入式竞赛板使用说明-V1.1里面最后一行也有标明!)

  • 低电平响,高电平不响
Buzzer STM32
N_Buz PB4
/*************************GPIO_init.h*************************/
# define BUZZER	GPIO_Pin_4
# define BeepOff() GPIO_SetBits(GPIOB, GPIO_Pin_4)	//关闭蜂鸣器
# define BeepOn() GPIO_ResetBits(GPIOB, GPIO_Pin_4)	//打开蜂鸣器

/*************************GPIO_init.c*************************/
//蜂鸣器初始化
void BUZZInit(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;	//定义结构体

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO|RCC_APB2Periph_GPIOB,ENABLE);	//使能复用功能,B组外设时钟

    GPIO_PinRemapConfig(GPIO_Remap_SWJ_NoJTRST,ENABLE);	//除JTRST外SWJ完全使能(JTAG+SW-DP)用于嵌入式使用蜂鸣器时需要加
    GPIO_InitStructure.GPIO_Pin = BUZZER;	//PB^4
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;	//推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB,&GPIO_InitStructure);

    BeepOff();	//关闭蜂鸣器
}

/*************************BEEP.h*************************/
void Buzz_display(void);

/*************************BEEP.c*************************/
# include "BUZZ.h"
# include "DELAY.h"

void Buzz_display(void)
{
    GPIO_ResetBits(GPIOB,GPIO_Pin_4);	//开启
    delay_ms(200);
    GPIO_SetBits(GPIOB,GPIO_Pin_4);	//关闭
}

/*************************main.c*************************/
//略直接调用即可

EEPORM

EEPROM STM32
SCL PB6
SDA PB7
E0~E2 GND
  • 比赛已经给了 i2c.ci2c.h 文件了,直接复制到自己工程下,然后在 .h 文件里包含 stm32f10x.h 在 .c 文件包含 i2c.h 即可(以后哪个文件需要调用到I2C函数可以在头文件处包含 i2c.h)

24C02芯片

  • 24C02是 2Kbits,一共 256 字节,8 个字节一个页,所以可寻址范围是 0x00~0xFF(1kbits=1024bits=(1024/8)Bytes=128Bytes(字节))

可参考另一篇文章:

[post cid=“101” cover=“https://image-1309791158.cos.ap-guangzhou.myqcloud.com/其他/QQ截图20221003140600.webp” /]

  • E0~E2 接 GND(即0)
  • I2C传输是 高位在前,低位在后!!!
器件地址 "写"地址 "读"地址
0101 0000 (0x50) 1010 0000 (0xA0) 1010 0001 (0xA1)
//EEPROM写一个字节 步骤
开始信号
写操作
等待应答
发送存储地址
等待应答
发送数据
等待应答
结束信号  
//EEPROM读一个字节 步骤
定义一个临时变量    
开始信号
写操作
等待应答
发送存储地址
等待应答
开始信号
读操作
等待应答
接收数据存到临时变量
等待应答
返回临时变量    

部分代码:

《功能是实现第一次上电显示1234,然后+1保存回去,之后每次上电再从内存读取出来显示在LCD》

  • 注意写是先低再高,读也是

eeprom.c

/*************************EEPROM.h*************************/
void eeprom_write_byte(u8 add,u8 data);
u8 eeprom_read_byte(u8 add);
void eeprom_write_u16(u8 add,u16 data);
u16 eeprom_read_u16(u8 add);

/*************************EEPROM.c*************************/
# include "EEPROM.h"
# include "i2c.h"
# include "DELAY.h"

/* -------------------------------- begin  ------------------------------ */
/**
  * @函数名:  eeprom_write_byte
  * @参数1 : 待写入的数据存放地址
  * @参数2 : 待写入的数据
  * @返回值: 无
  * @时  间:  2022-10-27
  * @描  述: 往EEPROM里写入一个字节数据
 **/
/* -------------------------------- end -------------------------------- */
void eeprom_write_byte(u8 add,u8 data)
{
    I2CStart();	//开始信号
    I2CSendByte(0xA0);	//写操作
    I2CWaitAck();	//等待应答

    I2CSendByte(add);	//发送地址
    I2CWaitAck();	//等待应答

    I2CSendByte(data);	//发送数据
    I2CWaitAck();	//等待应答
    I2CStop();	//结束信号
}

/* -------------------------------- begin  ------------------------------ */
/**
  * @函数名:  eeprom_read_byte
  * @参数1 : 待读出的数据存放地址
  * @返回值: 读到的数据(u8类型)
  * @时  间:  2022-10-27
  * @描  述: 从EEPROM里读出一个字节数据
 **/
/* -------------------------------- end -------------------------------- */
u8 eeprom_read_byte(u8 add)
{
    u8 data;

    I2CStart();	//开始信号

    I2CSendByte(0xA0);	//写操作
    I2CWaitAck();	//等待应答

    I2CSendByte(add);	//发送数据所在地址
    I2CWaitAck();	//等待应答

    I2CStart();	//开始信号
    I2CSendByte(0xA1);	//读操作
    I2CWaitAck();	//等待应答

    data = I2CReceiveByte();	//接收数据
    I2CSendAck();	//等待应答

    return data;	//返回数据
}

/* -------------------------------- begin  ------------------------------ */
/**
  * @函数名:  eeprom_write_u16
  * @参数1 : 待写入的数据存放地址
  * @参数2 : 待写入的数据(u16类型)
  * @返回值: 无
  * @时  间:  2022-10-27
  * @描  述: 往EEPROM里写入2个字节数据
 **/
/* -------------------------------- end -------------------------------- */
void eeprom_write_u16(u8 add,u16 data)
{
    u8 Hn,Ln;
    Hn = (data>>8)&0xFF;	//获取高位
    Ln = data&0xFF;	//获取低8位
    eeprom_write_byte(add,Ln);	//发送低位
    delay_ms(5);
    eeprom_write_byte(add+1,Hn);	//发送高位
    delay_ms(5);
}

/* -------------------------------- begin  ------------------------------ */
/**
  * @函数名:  eeprom_read_u16
  * @参数1 : 待读出的数据存放地址
  * @返回值: 读到的数据(u16类型)
  * @时  间:  2022-10-27
  * @描  述: 从EEPROM里读出2个字节数据
 **/
/* -------------------------------- end -------------------------------- */
u16 eeprom_read_u16(u8 add)
{
    u16 tmp;
    tmp = (u16)eeprom_read_byte(add);
    delay_ms(5);
    tmp += (u16)(eeprom_read_byte(add+1)<<8);
    delay_ms(5);

    return tmp;
}

/*************************main.c*************************/
u16 num;	//EEPROM显示
int main(void)
{
    i2c_init();
    
    if(eeprom_read_byte(0x00)!=0x50)	//选择任意一处内存作为标志位判断是不是第一次上电
	{
		eeprom_write_u16(0x01,1233);	//写入初值
		delay_ms(5);
		eeprom_write_byte(0x00,0x50);	//写入标志位
		delay_ms(5);
	}
	num = eeprom_read_u16(0x01);
	num++;
	eeprom_write_byte(0x01,num);
}

//window函数里显示
void window(const char* arr)
{
    u8 str2[20];	//Line2
    
    sprintf((char*)str2,"%d",num);
	LCD_DisplayStringLine(Line2,str2);
}

客观题

待更新–2022.11.7

附1:中断问题

  • NVIC_PriorityGroupConfig() 函数在每个工程中只需要配置一次。如果整个工程均没有配置,那么默认配置为 VIC_PriorityGroup_00组>1组>2组>3组>4组
  • 关于抢占和响应,必须清楚:抢占优先级相同的中断(即同一组),高响应优先级不可以打断低响应优先级的中断。当抢占优先级相同,两个中断同时发生的情况下,响应优先级高的先执行。还有值越小,优先级越高。

  • 每个中断服务函数可以在 stm32f10x_md.s 里找

  • 配置步骤:

① 确定中断分组(一个工程只设置一次)

② 调用 NVIC_init() 配置中断函数

③ 写中断服务函数

附2:定时器问题

  • 高配有包括 2 个高级控制定时器4 个通用定时器2个基本定时器(TIM6/TIM7),以及 2 个看门狗定时器1 个系统定时器(SysTick)
芯片型号 高级定时器 通用定时器 基本定时器 滴答定时器
STM32F103RBT6 TIM1 TIM2,3,4 SysTick
  • 计数器寄存器(TIMx_CNT)(和51TH/TL一样的)、预分频寄存器(TIMx_PSC)自动重装载寄存器(TIMx_ARR)三部分构成。
  • 时钟源有三个,HSI、HSE 和 PLL
HSI - 由 内部 RC 振荡器产生8MHz,精度较差,对时钟精度不敏感的情况下使用;

HSE - 由 外部时钟源产生8MHz信号;

PLL - 是将 HSI 或者 HSE 的时钟倍频后提供的时钟频率
  • AHB 域和 APB2最大时钟频率为 72MHz,而 APB1 域允许的最大时钟频率为 36MHz

  • 基本定时器

TIM_Prescaler TIM_Period 基本定时器配置使用 TIM6 和 TIM7 的时候只需初始化这两个成员即可

  • 计数模式
    向上计数模式:从0开始,计到arr预设值,产生溢出事件,返回重新计时
    向下计数模式:从arr预设值开始,计到0,产生溢出事件,返回重新计时
    中央对齐模式:从0开始向上计数,计到arr产生溢出事件,然后向下计数,计数到1以后,又产生溢出,然后再从0开始向上计数。(此种技术方法也可叫向上/向下计数)

  • 高级定时器引脚

  • 通用定时器引脚
  • 配置

注意:重装载值和时钟预分频数计算公式:定时器时间计算公式为(arr+1)*(psc+1)/72MHz(单位:S),将参数 9、71 带入公式后,计算出定时器的定时周期值为10us

​ 根据定时器时钟的频率,比如时钟的频率是 72MHz,可以理解为一秒钟STM32会自己数 72M 次,预分频系数就是将频率分割,比如分频系数是 72,则该时钟的频率会变成72MHz/72=1MHz,但是在设置的时候要注意,数值应该是72-1。假定分频系数是72-1,那么频率变成 1MHz,也就意味着STM32在一秒钟会数 1M 次,即1us数一次。好了,接下来就是确定预装载值,比如需要定时1ms,由于1ms=1us*1000,那么预装载值就是1000-1;如此类推,在预分频系数确定的情况下,定时的时长就由预装载值确定了。至于要把值减一的原因,估计是计数是从0开始,所以要减一。(搭配有很多种,看个人需求)

理解:当AHB一分频(即不分频)时,HCLK就是最大频率72MHz,APB1预分频则为二分频,下面则2倍频,最终定时器频率为最大值72MHz!

注意:为什么是72MHz,因为系统时钟默认是这个,可以在 system_stm32f10x.c115行 找到,如果想换成别的可以把别的注释打开即可(当AHB提供的时钟源是72时APB1预分频最小需要配置成二分频)

滴答定时器

参考:STM32F10xxx Cortex-M3 programming manual手册4.5

CTRL(状态和控制寄存器 ,32位,复位值:0x0000 0004)

//位分析
31~17位: 保留,必须保持 0
16位:计数标志位,如果定时器自上次读取后计数为0,则返回1 
15~3位: 保留,必须保持 0
2位:时钟源选择, 0: AHB/8	1: 处理器时钟(AHB)   
1位:异常请求启用,0: 倒数到零不会断言SysTick异常请求	1:倒数到零时发出SysTick异常请求。    
0位:计数器启用,0:计数器禁用 1: 计数器启用(当ENABLE被设置为1时,计数器从LOAD寄存器中加载RELOAD值,然后向下计数。寄存器中加载RELOAD值,然后向下计数。当达到0时,它将COUNTFLAG设置为1,并可选择将SysTalk中断。并根据TICKINT的值选择性地断言SysTick。然后,它再次加载RELOAD )

LOAD(重装载值寄存器)

时钟频率*us(一般是72)

VAL(当前值寄存器,复位值:0x00)

[collapse status=“false” title=“delay.h”]

# ifndef __DELAY_H
# define __DELAY_H
# include "stm32f10x.h"


void delay_us(u32 us);
void delay_ms(u32 ms);
void delay_s(u16 s);

# endif
  • 延时延时使用系统滴答定时器,具体可观看博客:STM32 的系统滴答定时器( Systick) 彻底研究解读
  • 延时前计数器值清0,解决连续用us延时的BUG问题
  • 当计数到零,SysTick->CTRL变成 0x10000,也就是它的第十六位置一,然后 &0x10000就为真,取非,变成假,跳出while循环

delay.c

# include "delay.h"

# define AHB_INPUT  72  //请按RCC中设置的AHB时钟频率填写到这里(单位MHz)

void delay_us(u32 us)//uS微秒级延时程序
{
	SysTick->LOAD=AHB_INPUT*us;//重装计数初值(当主频是72MHz,72次为1微秒)
	SysTick->VAL=0x00;//清空定时器的计数器
	SysTick->CTRL=0x00000005;//时钟源HCLK,打开定时器
	while(!(SysTick->CTRL&0x00010000));//等待计数到0
	SysTick->CTRL=0x00000004;//关闭定时器
}

void delay_ms(u32 ms)//mS毫秒级延时程序
{
	while(ms--!=0)
	{
		delay_us(1000);
	}
}

void delay_s(u16 s)//S秒级延时程序
{
	while(s--!=0)
	{
		delay_ms(1000);
	}
}

定时器初始化踩坑

固件库定时器初始化函数 默认会产生更新事件并触发更新事件中断标志位,如果此时中断使能,程序会立即执行中断函数。

如果使用标准库,先用 TIM_ClearITPendingBit(TIM1, TIM_IT_Update) 清除中断标记,然后使能定时器中断,最后启动定时器

如果使用HAL库,在MX_TIMx_Init函数后紧跟着__HAL_TIM_CLEAR_IT(&htimX, TIM_IT_UPDATE)以此来清除中断标识位

附3:开发板对应复用引脚

  • 开发板对应 CON10排针引出引脚复用表
管脚 复用功能
PA1 TIM2_CH2,ADC_IN1
PA2 TIM2_CH3,ADC_IN2,USART2_TX
PA3 TIM2_CH4,ADC_IN3,USART2_RX
PA4 ADC_IN4
PA5 ADC_In5
PA6 TIM3_CH1,ADC_IN6
PA7 TIM3_CH2,ADC_IN7
管脚 功能
PB6 PB6/I2C1_SCL/TIM4_CH1
PB7 PB7/I2C1_SDA/TIM4_CH2
PB8 PB8/TIM4_CH3
PB9 PB9/TIM4_CH4
  • 对应功能的IO模式选择
所处功能 对应模式
PWM输出 GPIO_Mode_AF_PP(复用推挽输出)
PWM输入捕获 GPIO_Mode_IN_FLOATING(浮空输入)
  • TIM2_CH1_ETR

  • TIM1_CH2N 引脚是定时器1通道2互补输出引脚

什么时候用到打开AFIO呢?

① 首先要有管脚复用功能AFIO;
② 其次被复用的管脚一定是挂载在APB2上的,因为AFIO就是在APB2上;
③ 最后就是内置外设一定是APB2表中没有的,因为APB2有的话,直接打开就好了,也用不到打开AFIO;
④ 根据以上条件,打开AFIO的只有一种情况,那就是: 挂载在APB1下的内置外设,经过重映射功能,把管脚映射到APB2上!其实,一旦使用重映射功能,只能映射到APB2上,因为APB2表中第二个框子里面包括了GPIOA~E,几乎所有的管脚了

附:PWM问题

首先需要了解几个知识:

PWM:脉宽调制信号;

占空比:占空比是指 有效电平 在一个周期之内所占的时间比率。如果方波的占空比为60%,占空比为0.6,即信号在60%时间内是导通的,但在40%的时间内处于断态

方波:跟占空比差不多,一般用百分数表示,占空比0.5就是方波50%

pwm的频率:是指每秒钟信号从高电平到低电平再回到高电平的次数

那定时器在这的作用是什么?答:利用定时器的溢出可设定总周期,高低电平则另一个功能去完成实现

实际输出的电平是由输出极性和电平是否有效共同决定的

什么时候是有效电平?什么时候是无效电平呢?这个其实是和实际的配置模式有关,常用的PWM模式有 PWM模式1和PWM模式2

惯用的搭配是 TIM_OCMode_PWM1TIM_OCPolarity_High 一对, TIM_OCMode_PWM2TIM_OCPolarity_Low 一对,这样搭配是正逻辑 ,否则为反逻辑

需要注意输出100%和0%的占空比会有不准确

下面表以通道1为例:

计数方式 PWM1(模式1) PWM2(模式2)
向上/向下 CNT < CCR1 —> 通道1为有效电平
CNT > CCR1 —> 通道1为无效电平
CNT < CCR1—> 通道1为无效电平
CNT > CCR1—> 通道1为有效电平

名词解释:

名词 解释
CNT 计数器寄存器
CCRx 即TIM_Pulse (决定了占空比)
极性 高电平/低电平 (决定了PWM有效电平)
ARR 溢出值 (决定了PWM频率)

单位换算:

Hz(赫兹),KHz(千赫兹),MHz(兆赫兹),GHz(G赫兹) s(秒),ms(毫秒),us(微秒),ns(纳秒)
 1GHz = 1000MHz
1MHz = 1000KHz
1KHz = 1000Hz
 1s = 1000ms
1ms = 1000us
1us = 1000ns
1 Hz — 1s
1KHz — 1ms
1 MHz — 1us
1GHz — 1ns
左边越大,对应时间越小(左边除以2则右边乘以2)
计算公式
TIM_Period (arr) (arr+1)/(72000000/(psc+1) = xxxS(根据实际定时多少秒),这个结果也等于PWM频率
TIM_Prescaler (psc) 同上
占空比(%) Duty = CCRx/(arr+1) x 100%
CCRx 1000000/频率(Hz)
占空比所占计数值 CCRx * 占空比(1~99)/100

假设此时配置为 PWM模式2输出高极性 ,如图所示,当 TIMx_CNT < TIMx_CCRx 的时候,是无效电平,那么输出为0(即低电平);当 TIMx_CNT > TIMx_CCRx 的时候,是 有效电平,那么输出为1(即高电平)

  • 6种模式

  • 关于TIM_OC1PreloadConfig 是否使能(PWM模式无所谓,但是比较输出一定要禁用或不写)

如果 使能 了该位,写入到TIMx_CCRx寄存器的比较值将在更新事件到来时才会传入到 当前捕获/比较寄存器,否则 未使能,比较值将 立即写入当前捕获比较寄存器(输出比较模式一般disable或不写)

高级定时器(TIM1)使用注意

  • 根据TIM的四个捕获比较通道位置: PA8/PA9/PA10/PA11。再结合实际电路,确定了选择 CH2-PA9CH3-PA10 作为PWM输出,但是此时要注意,拔掉对应的跳线帽
  • 高级定时器必须加这一句TIM_CtrlPWMOutputs(TIM1, ENABLE);//使能PWM输出

附5:DMA知识

  • 每个外设都有对应的DMA 传输通道,这两张表的内容即用即查。(手册10.3.7)

DMA1通道映射表:

外设 通道1 通道2 通道3 通道4 通道5 通道6 通道7
ADC1 ADC1
SPI/I2S SPI1_RX SPI1_TX SPI/I2S2_RX SPI/I2S2_TX
USART USART3_TX USART3_RX USART1_TX USART1_RX USART2_RX USART2_TX
I2C I2C2_TX I2C2_RX I2C1_TX I2C1_RX
TIM1 TIM1_CH1 TIM1_CH2 TIM1_TX4
TIM1_TRIG
TIM1_COM
TIM1_UP TIM1_CH3
TIM2 TIM2_CH3 TIM2_UP TIM2_CH1 TIM2_CH2
TIM2_CH4
TIM3 TIM3_CH3 TIM3_CH4
TIM3_UP
TIM3_CH1
TIM3_TRIG
TIM4 TIM4_CH1 TIM4_CH2 TIM4_CH3 TIM4_UP

DMA2通道映射表:

外设 通道1 通道2 通道3 通道4 通道5
ADC3 ADC3
SPI/I2S3 SPI/I2S3_RX SPI/I2S3_TX
USART4 USART4_RX USART4_TX
SDIO SDIO
TIM5 TIM5_CH4
TIM5_TRIG
TIM5_CH3
TIM5_UP
TIM5_CH2 TIM5_CH1
TIM6
DAC通道1
TIM6_UP
DAC通道1
TIM7
DAC通道2
TIM7_UP
DAC通道2
TIM8 TIM8_CH3
TIM8_UP
TIM8_CH4
TIM8_TRIG
TIM8_COM
TIM8_CH1 TIM8_CH2

如何理解DMA?

对于DMA,打个比方就很好理解:
角色预设:

淘宝店主 —- STM32 MCU
快递员 —- 外设(如UART,SPI)
发货室 —- DMA
1、首先你是一个淘宝店主,如果每次发货收货都要跟快递沟通交涉会很浪费时间和精力。
2、然后你就自己建了一个发货室,发货室里有好多个货柜箱子,每个箱子上都写着快递名字(如果申通快递,顺丰快递等)。
3、每次发什么快递,你就找到对应的货柜箱子,把货物放进去即可,然后跟快递通知一声。
4、快递取走快件。
5、如果是收货,快递直接把快件放到对应的柜子,然后通知你一下。
6、你过来提取货物。

通过上面的方式,你可以不需要直接跟快递打交道,就可以轻松发货成功,DMA处理方式跟上面例子是一样的。

DMA_InitTypeDef
结构体成员
描述 取值范围 理解
DMA_PeripheralBaseAddr DMA传输的外设基地址,一般为外设
的数据寄存器地址,如果使用存储器到存储器传输,
则为其中一个存储器的基地址
(u32)
一般是外设起始地址+DR寄存器偏移量
相当于“哪家快递”
DMA_MemoryBaseAddr DMA传输的存储器基地址,通常是
程序开辟的一段内存的起始地址。
(u32)&数组名 相当于几号柜
DMA_DIR 外设是作为数据传输的目的地还是来源 DMA_DIR_PeripheralDST
DMA_DIR_PeripheralDST
作为数据来源,即为收快递;作为目的地即为发快递
DMA_BufferSize 可以理解为数组的大小 / 缓存容量,即柜子大小
DMA_PeripheralInc 外设地址寄存器是否递增 DMA_PeripheralInc_Enable
DMA_PeripheralInc_Disable
不递增,即柜子对应的快递不变
DMA_MemoryInc 内存地址是否递增 DMA_PeripheralInc_Enable
DMA_PeripheralInc_Disable
不递增,即一直把快递放同一个柜子
DMA_PeripheralDataSize 外设数据宽度(8,16,32位数据宽度) DMA_MemoryDataSize_Byte
DMA_MemoryDataSize_HalfWord
DMA_MemoryDataSize_Word
即快递运输快件大小度量
DMA_MemoryDataSize 定义的数组大小(8,16,32位数据宽度) 同上 即店主封装快递的度量
DMA_Mode DMA工作模式(循环/非循环模式) DMA_Mode_Circular
DMA_Mode_Normal
正常模式,即满了就不再接收了,而不是循环存储
DMA_Priority 通道x的软件优先级(非常高,高,中,低优先级) DMA_Priority_VeryHigh
DMA_Priority_High
DMA_Priority_Medium
DMA_Priority_Low
相当于快递的加急,很急,急,一般;快递员优先拿加急的
DMA_M2M 是否使能DMA通道的内存到内存传输 DMA_M2M_Enable
DMA_M2M_Disable
/

附6:Keil调试查看PWM波形

  • 打开 魔法棒debug – 选择《Use Simulator》,打勾《Run to main()》;-- Target – 晶振那写“8.0”
  • 在你需要测试的PWM函数前设断点,再点击右上角那个 带d字放大镜Setup创建图标 – 输入 “PORTX.Y” (即你PWM通道对应管脚,X对应端口组,Y对应哪个管脚1~13) – Display Type选 “Bit” – 颜色的话随便就行 – close
  • 点击左边 RST(重置),然后点击旁边的 run(运行),它会运行到你断点处,此时再点击第二个括号,直到出现波形图即可,勾《Signal Info》然后把鼠标放在某一处会出现对应信息(时间,Hz等),勾《Cursor》,这个可以测某处到另一处之间的时间(看d:),先点开始处在移动蓝线即可