蓝桥杯嵌入式模块详解(旧板)
前言
此笔记有参考了各博主文章:
https://blog.csdn.net/Zach_z/article/details/80548423
https://blog.csdn.net/Zach_z/article/details/80548423
https://recclay.blog.csdn.net/article/details/86594683
https://zhuanlan.zhihu.com/p/489165065
手册:
在阿里云盘—我的手册(已凃)
板子介绍
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~LED8,PD2 对应 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.c 里1095行左右
- 我们想配置 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_239Cycles5
239.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比较模式: 同一定时器下,不同通道能够产生频率不同,占空比不同,甚至相位也不同的方波
输出比较
:定时器的计数值一直向上计数的同时,他要不断的跟我们设定的 pulse
和 Duty
做比较
什么是 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.c
和i2c.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_0;0组>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)
(和51
的TH/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.c
里 115行
找到,如果想换成别的可以把别的注释打开即可(当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_PWM1
和 TIM_OCPolarity_High
一对, TIM_OCMode_PWM2
和 TIM_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-PA9
和CH3-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:),先点开始处在移动蓝线即可