STM32归档(上)
个人学习笔记,将对知识点进行概述,主要学习请参考B站江科大的视频。或者STM32手册。
GPIO
GPIO简介
•GPIO(General Purpose Input Output)通用输入输出口
•可配置为8种输入输出模式
•引脚电平:0V~3.3V,部分引脚可容忍5V
•输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等
•输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等
GPIO基本结构

所有GPIO都挂载在APB2总线上。PA0~PB15为GPIO外设引脚
寄存器是一段特殊的存储器,内核可以通过APB2总线对寄存器进行读写,以完成输出电平和读取电平的功能,但只有低16位才有对应端口。
驱动器负责增大驱动能力。
GPIO位结构

GPIO模式

推挽输出的高低电平均有较强的驱动能力。
浮空/上拉/下拉输入

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

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

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

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

示例程序
流水灯
#include "stm32f10x.h" // Device header
#include "Delay.h" // 延时函数
int main(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启APB2时钟
GPIO_InitTypeDef GPIO_InitStructure; //为结构体命名
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //设为GPIO为推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All; //所有引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //输出速度
GPIO_Init(GPIOA, &GPIO_InitStructure);
while (1)
{
GPIO_Write(GPIOA, ~0x0001); //0000 0000 0000 0001 //一次写入16个端口,控制GPIO
Delay_ms(100);
GPIO_Write(GPIOA, ~0x0002); //0000 0000 0000 0010
Delay_ms(100);
GPIO_Write(GPIOA, ~0x0004); //0000 0000 0000 0100
Delay_ms(100);
GPIO_Write(GPIOA, ~0x0008); //0000 0000 0000 1000
Delay_ms(100);
GPIO_Write(GPIOA, ~0x0010); //0000 0000 0001 0000
Delay_ms(100);
GPIO_Write(GPIOA, ~0x0020); //0000 0000 0010 0000
Delay_ms(100);
GPIO_Write(GPIOA, ~0x0040); //0000 0000 0100 0000
Delay_ms(100);
GPIO_Write(GPIOA, ~0x0080); //0000 0000 1000 0000
Delay_ms(100);
}
}
LED闪烁
#include "stm32f10x.h" // Device header
#include "Delay.h" //延时函数
int main(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启时钟
GPIO_InitTypeDef GPIO_InitStructure; //结构体,初始化GPIO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //初始化的引脚为GPIOA的pin0
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //运行速度
GPIO_Init(GPIOA, &GPIO_InitStructure);
while (1)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_0); //把pin0置为低电平
Delay_ms(500);
GPIO_SetBits(GPIOA, GPIO_Pin_0); //把pin0置为高电平
Delay_ms(500);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);
Delay_ms(500);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);
Delay_ms(500);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)0);
Delay_ms(500);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)1);
Delay_ms(500);
}
}
NVIC
中断系统
中断: 在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行
中断优先级: 当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源
中断嵌套: 当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回
中断执行流程


程序由硬件电路自动跳转到中断程序中
STM32中断
•68个可屏蔽中断通道,包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设
•使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级。
内核中断:

NVIC的基本结构

一个外设可能占用多个中断通道
NVIC优先级分组
•NVIC的中断优先级由优先级寄存器的4位(0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级
•抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队

EXTI外部中断
EXTI简介
•EXTI(Extern Interrupt)外部中断
•EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序
•支持的触发方式:上升沿/下降沿/双边沿/软件触发
•支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断。(PA1和PB1不能同时用)
•通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒
•触发响应方式:中断响应/事件响应
中断响应:让CPU执行中断函数
事件响应:当外部中断检测到引脚变化时,中断信号就不会通向CPU了,而是通到其它外设,用来触发其它外设的操作(ADC,DMA)属于外设之间的联合工作。
EXTI的基本结构

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

EXTI框图

示例程序
配置过程:
1. 配置RCC,把涉及的外设时钟都打开GPIO,AFIO,(无需:NVIC,EXTI)
2. 配置GPIO,选择端口为输入模式
3. 配置AFIO,选择所需的GPIO链接EXTI。
4. 配置EXTI,选择触发方式和响应方式。(一般为边沿触发,中断响应)
5. 配置NVIC,选择合适的优先级。
最后,通过NVIC,外部中断信号进入CPU。
对射式红外传感器计次

挡光片在对射式红外传感器中间经过时,D0输出电平跳变,触发PB14号IO口的中断。
代码
#include "stm32f10x.h" // Device header
uint16_t CountSensor_Count;
void CountSensor_Init(void)
{
//开启相关时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
//利用结构体配置GPIO
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
//选择中断线
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
//利用结构体配置EXTI
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line14;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);
//利用结构体配置NVIC
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
}
uint16_t CountSensor_Get(void)
{
return CountSensor_Count;
}
void EXTI15_10_IRQHandler(void) //中断函数,函数名不可自定义
{
if (EXTI_GetITStatus(EXTI_Line14) == SET)
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
{
CountSensor_Count ++;
}
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
螺旋编码器可用于测量位置,速度或旋转方向,当其旋转轴旋转时,其输出端额可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度与方向。


默认无旋转时,此时上拉为高电平,并输出到A端口,当旋转时内部触电导通,此时因为连接GND,A端口就为低电平。
旋转轴旋转时,此时两触点以相位相差90度的方式交替导通。
示例连接图:

主函数
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Encoder.h"
int16_t Num;
int main(void)
{
OLED_Init(); //初始化OLED屏幕
Encoder_Init();
OLED_ShowString(1, 1, "Num:"); //OLED显示函数
while (1)
{
Num += Encoder_Get();
OLED_ShowSignedNum(1, 5, Num, 5);
}
}
Encoder.c
#include "stm32f10x.h" // Device header
int16_t Encoder_Count;
//初始化,配置相关IO口,中断。
void Encoder_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);
GP_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&NVIC_InitStructure);
}
//放回count的变化值
int16_t Encoder_Get(void)
{
int16_t Temp;
Temp = Encoder_Count;
Encoder_Count = 0;
return Temp;
}
//中断程序,执行计数
void EXTI0_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line0) == SET)
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
Encoder_Count --;
}
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
void EXTI1_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line1) == SET)
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
{
Encoder_Count ++;
}
}
EXTI_ClearITPendingBit(EXTI_Line1);
}
}
定时器
TIM简介
•TIM(Timer)定时器
•定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断
•16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时
•不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能
•根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型
应用:利用输出比较功能,可以产生PWM波形,驱动电机。输入捕获功能,可以实现测量方波频率。定时器的编码器接口,也可以更方便的读取正交编码器的输出波形。
定时器类型

高级定时器可用于三相无刷电机。
STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4。
基本定时器

基本定时器一般是连接到内部时钟(72MHz),经过预分频器进行分频。比如:预分频器写0就是不分频或者(1分频),输出频率=输入频率=72M,写1就是二分频,输出=输入/2=36M,以此类推。这个预分频器是16位,最大值可填65535,也就是65536分频。分频后计数器(16位)会不断自增,通过自动重装寄存器(16位)可以控制计数时间。当计数=自动重装值,计数清零,输出中断。
主从模式触发ADC
能让内部的硬件在不受程序的控制下实现自动运行。
通用定时器


高级定时器


定时器结构详解
定时中断基本结构图

预分频器时序

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

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

计数器有预装时序

如果引入影子寄存器,当自动加载寄存器的值更改后,当前计数周期结束后,才会更改自动加载寄存器的值。实际上为了同步,让值的变化和事件的更新同步发生,防止运行途中由于更改所造成的错误。
比如:原本计时50秒,闹钟响起。计时到45秒时,改为计时到30秒响起闹钟。那么则是45->60->0->30,闹钟才会想起。引入影子寄存器后,则是45->50,响起闹钟,下一个周期:0->30再响起一次闹钟。
RCC时钟树
STM32用来产生和配置时钟.并吧配置好的时钟发送到各个外设系统。SystemInit就是用来配置时钟树的。

左边是产生时钟电路,右边则是分配电路。两个高速时钟是为系统提供时钟的。一般外部晶振比内部稳定,所以一般使用外部晶振。
SystemInit配置时钟过程:先开启内部8Mhz晶振作为系统时钟,再开启外部时钟(一般为8M),经过倍频且稳定后变为72M,再代替原来内部的8M作为系统时钟。所以,如果外部时钟出了问题,则切换为内部时钟。8M和72M相比,大概慢了10倍。
示例程序
配置过程(内部时钟为例):
1. RCC开启时钟,此时基准时钟和整个外设的工作时钟都被打开。
2. 选择时基单元的时钟源(内部时钟模式)
3. 配置时基单元
4. 配置输出中断控制,允许更新中断输出到NVIC
5. 配置NVIC,打开中断通道,并分配优先级
定时器中断(内部时钟):
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main(void)
{
OLED_Init();
Timer_Init();
//
OLED_ShowString(1, 1, "Num:");
while (1)
{
OLED_ShowNum(1, 5, Num, 5); // 在OLED屏幕上显示变量Num的值,位于第1行第5列,总宽度为5个字符
}
}
//
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
Num++; // 每次定时器2的更新中断触发时,增加Num的值
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除定时器2的更新中断标志位,准备下一次触发
}
}
Timer.c
#include "stm32f10x.h" // 包含STM32库的头文件
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 启用TIM2的时钟
TIM_InternalClockConfig(TIM2); // 配置TIM2的时钟源为内部时钟
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //结构体
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 不分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1; // ARR自动重装载寄存器的值,决定了计数器溢出的时间间隔
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; // PSC预分频器的值,决定了计数器的时钟频率
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(只有高级定时器才有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); // 初始化TIM2的时间基准单元
TIM_ClearFlag(TIM2, TIM_FLAG_Update); // 清除TIM2的更新标志位
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 启用TIM2的更新中断
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 配置NVIC的优先级分组为组2
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; // 配置中断通道为TIM2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 启用中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 配置抢占优先级为2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 配置子优先级为1
NVIC_Init(&NVIC_InitStructure); // 初始化NVIC
TIM_Cmd(TIM2, ENABLE); // 启用TIM2定时器
}
/*
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
// 在这里添加处理TIM2更新中断的代码
// 可以在此处执行你的特定任务或操作
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除TIM2的更新中断标志位
}
}
*/
Timer.h
#ifndef __TIMER_H
#define __TIMER_H
//
void Timer_Init(void);
//
#endif
定时器外部时钟
使用对射式红外传感器来模拟外部时钟。
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
//
uint16_t Num;
//
int main(void)
{
OLED_Init();
Timer_Init();
OLED_ShowString(1, 1, "Num:");
OLED_ShowString(2, 1, "CNT:");
while (1)
{
OLED_ShowNum(1, 5, Num, 5);
OLED_ShowNum(2, 5, Timer_GetCounter(), 5);
}
}
//
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
Num++; // 每次定时器2的更新中断触发时,增加Num的值
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除定时器2的更新中断标志位,准备下一次触发
}
}
Timer.c
#include "stm32f10x.h"
//
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 启用TIM2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 启用GPIOA的时钟
//
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 配置GPIOA的引脚模式为上拉输入模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 配置GPIOA的引脚为引脚0
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 配置GPIOA的引脚速度为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIOA
// 配置TIM2的外部时钟源模式
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F);
//结构体配置
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 不分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
TIM_TimeBaseInitStructure.TIM_Period = 10-1; // 自动重装载寄存器的值,决定了计数器溢出的时间间隔
TIM_TimeBaseInitStructure.TIM_Prescaler = 1-1; // 预分频器的值,决定了计数器的时钟频率
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); // 初始化TIM2的时间基准单元
//
TIM_ClearFlag(TIM2, TIM_FLAG_Update); // 清除TIM2的更新标志位
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 启用TIM2的更新中断
//
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 配置NVIC的优先级分组为组2
//
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; // 配置中断通道为TIM2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 启用中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 配置抢占优先级为2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 配置响应优先级为1
NVIC_Init(&NVIC_InitStructure); // 初始化NVIC
//
TIM_Cmd(TIM2, ENABLE); // 使能TIM2定时器
}
//
uint16_t Timer_GetCounter(void)
{
return TIM_GetCounter(TIM2); // 获取TIM2计数器的当前值
}
/*
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
// 在这里添加处理TIM2更新中断的代码
// 可以在此处执行你的特定任务或操作
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除TIM2的更新中断标志位
}
}
*/
Timer.h
#ifndef __TIMER_H
#define __TIMER_H
//
void Timer_Init(void);
uint16_t Timer_GetCounter(void);
//
#endif
PWM
PWM简介
• PWM(Pulse Width Modulation)脉冲宽度调制
• 在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域
• PWM参数:
频率 = 1 / TS 占空比 = TON / TS 分辨率 = 占空比变化步距
简单说,Ts代表一个高低电平变换周期的时间,占空比决定PWM等效出来的模拟电压的大小,分辨率则可以理解为变化的精细,比如:1% → 2%,分辨率为1%。1.1% → 1.2%,分辨率为0.1%


PWM需要用到输出比较,所以先介绍一下stm32的输出比较功能。
输出比较简介
• OC(Output Compare)输出比较
• IC(Input Capture) 输入捕获
• CC(Capture/Compare)输出比较和输入捕获
• 输出比较可以通过比较CNT与CCR寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形
• 每个高级定时器和通用定时器都拥有4个输出比较通道
• 高级定时器的前3个通道额外拥有死区生成和互补输出的功能
输出比较通道(高级)

输出比较通道(通用)

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

PWM基本结构

参数计算:
• PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
• PWM占空比: Duty = CCR / (ARR + 1)
• PWM分辨率: Reso = 1 / (ARR + 1)
示例程序
配置PWM初始化程序的一般步骤
1. 开启TIM外设和GPIO外设的时钟。
2. 配置时基单元。
3. 配置输出比较单元。CCR的值,输出比较模式,极性选择,输出使能
4. 配置GPIO,PWM对应的GPIO,配置为复用推挽输出
5. 运行控制
PWM驱动LED呼吸灯
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
//
uint8_t i;
//
int main(void)
{
OLED_Init();
PWM_Init();
//
while (1)
{
// 增加PWM占空比从0到100,使亮度逐渐增大
for (i = 0; i <= 100; i++)
{
PWM_SetCompare1(i); // 设置PWM通道1的占空比
Delay_ms(10);
}
// 降低PWM占空比从100到0,使亮度逐渐减少
for (i = 0; i <= 100; i++)
{
PWM_SetCompare1(100 - i); // 设置PWM通道1的占空比
Delay_ms(10);
}
}
}
PWM.c
#include "stm32f10x.h"
void PWM_Init(void)
{
// 启用TIM2时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 启用GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置TIM2通道1的GPIO引脚
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 配置的GPIO引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置TIM2为内部时钟
TIM_InternalClockConfig(TIM2);
// 配置TIM2的时基
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; // ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; // PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
// 配置TIM2通道1为PWM输出
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0; // CCR
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
// 启用TIM2
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompare1(uint16_t Compare)
{
// 设置TIM2通道1的比较值
TIM_SetCompare1(TIM2, Compare);
}
PWM.h
#ifndef __PWM_H
#define __PWM_H
//
void PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);
//
#endif
PWM驱动舵机
• 舵机是一种根据输入PWM信号占空比来控制输出角度的装置
• 输入PWM信号要求:周期为20ms,高电平宽度为0.5ms~2.5ms
main.c
#include "stm32f10x.h"
#include "Delay.h" //延时
#include "OLED.h"
#include "Servo.h"
#include "Key.h" //按键
// 全局变量声明
uint8_t KeyNum;
float Angle;
int main(void)
{
// 初始化OLED显示屏、舵机和按键
OLED_Init();
Servo_Init();
Key_Init();
OLED_ShowString(1, 1, "Angle:");
while (1)
{
// 获取按键状态
KeyNum = Key_GetNum();
// 如果按键被按下
if (KeyNum == 1)
{
Angle += 30; // 增加角度值
if (Angle > 180)
{
Angle = 0; // 限制在0到180度之间
}
}
// 设置舵机的角度
Servo_SetAngle(Angle);
// 在OLED上显示当前角度
OLED_ShowNum(1, 7, Angle, 3);
}
}
Servo.c
#include "stm32f10x.h" // Device header
#include "PWM.h"
//初始化,和示例一的PWM初始化一致
void Servo_Init(void)
{
PWM_Init();
}
//设置PWM脉冲宽度,并且与角度用公式转换,便于写入和理解
void Servo_SetAngle(float Angle)
{
PWM_SetCompare2(Angle / 180 * 2000 + 500);
}
输入捕获
输入捕获简介
• IC(Input Capture)输入捕获
• 输入捕获模式下,当通道输入引脚出现指定电平跳变时,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数
• 每个高级定时器和通用定时器都拥有4个输入捕获通道
• 可配置为PWMI模式,同时测量频率和占空比
• 可配合主从触发模式,实现硬件全自动测量
频率测量


输入捕获通道

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

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

只用了一个通道,所以只能测频率
PWMI基本结构

使用2个通道同时捕获一个引脚,CRR1为一整个周期的计数值,CRR2是高电气期间的计数值。则占空比=CCR2/CCR1
示例程序
一般配置步骤
1.使用RCC开启TIM和GPIO的时钟。
2.初始化GPIO,配置为输入模式,一般为上拉/浮空输入
3.配置时基单元,使CNT计数器在内部时钟的驱动下自增运行
4.配置输入捕获单元(包括滤波器,极性选择,直连通道)
5.选择从模式的触发源,选择TI1FP1。
6.选择从模式的触发动作(Reset,自动清零CNT)
7.调用TIM_cmd()启动定时器计数。
输入捕获模式测频率
使用STM32自身IO口,PA0输出信号,PA6接收并检测频率。
main.c
#include "stm32f10x.h"
// 自定义头文件,相关程序参考文章前置相关
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
//
int main(void)
{
// 初始化
OLED_Init();
PWM_Init();
IC_Init();
OLED_ShowString(1, 1, "Freq:00000Hz");
// 设置PWM的预分频器和占空比
PWM_SetPrescaler(720 - 1); // Freq = 72M / (PSC + 1) / 100
PWM_SetCompare1(50); // Duty = CCR / 100
while (1)
{
// 获取输入捕获模块测得的频率,并在OLED上显示
OLED_ShowNum(1, 6, IC_GetFreq(), 5);
}
}
IC.c
#include "stm32f10x.h"
// 初始化输入捕获模块
void IC_Init(void)
{
// 启用TIM3时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// 启用GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置GPIO,为上拉输入模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置TIM3为内部时钟
TIM_InternalClockConfig(TIM3);
// 配置TIM3的时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; // ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; // PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
// 配置TIM3通道1的输入捕获参数
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter = 0xF;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
// 配置TIM3触发源和从模式
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
// 启用TIM3
TIM_Cmd(TIM3, ENABLE);
}
// 获取输入捕获模块测得的频率
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1);
}
PWMI模式测频率占空比
使用STM32自身IO口,PA0输出信号,PA6接收并检测频率和占空比。
main.c
#include "stm32f10x.h"
//自定义头文件
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
int main(void)
{
// 初始化
OLED_Init();
PWM_Init();
IC_Init();
// OLED显示信息
OLED_ShowString(1, 1, "Freq:00000Hz");
OLED_ShowString(2, 1, "Duty:00%");
// 设置PWM的预分频器和占空比
PWM_SetPrescaler(720 - 1); // Freq = 72M / (PSC + 1) / 100
PWM_SetCompare1(50); // Duty = CCR / 100
while (1)
{
// 获取输入捕获模块测得的频率和占空比,并在OLED上显示
OLED_ShowNum(1, 6, IC_GetFreq(), 5); // 显示频率
OLED_ShowNum(2, 6, IC_GetDuty(), 2); // 显示占空比
}
}
IC.c
#include "stm32f10x.h" // 包含STM32标准库头文件
// 初始化输入捕获模块
void IC_Init(void)
{
// 启用TIM3时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// 启用GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置GPIO为上拉输入
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; // 配置的GPIO引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置TIM3为内部时钟
TIM_InternalClockConfig(TIM3);
// 配置TIM3的时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; // ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; // PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
// 配置TIM3通道1的输入捕获参数
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter = 0xF;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_PWMIConfig(TIM3, &TIM_ICInitStructure); //该函数只需传入一个参数,会自动把剩下的通道初始化为相反的配置,仅适用于通道1和通道2
// 配置TIM3触发源和从模式
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
// 启用TIM3计数器
TIM_Cmd(TIM3, ENABLE);
}
// 获取输入捕获模块测得的频率
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1);
}
// 获取输入捕获模块测得的占空比
uint32_t IC_GetDuty(void)
{
return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1);
}
编码器接口
编码器接口简介
•Encoder Interface 编码器接口
•编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度
•每个高级定时器和通用定时器都拥有1个编码器接口
•两个输入引脚借用了输入捕获的通道1和通道2(CH1和CH2引脚)
正交编码器
正交编码器可以输出两个相位相差90°的方波信号。接入stm32,可看成一个带有方向控制的外部时钟。

使用正交信号精度更高,相当于AB相都参与计次,提高计次频率的同时,也可以抗干扰。
编码器接口基本结构

工作模式

实例(均不反相)

实例(TI1反相)

如果接入编码器后,需要调整数据加减的方向,可直接调用极性选择。
示例程序
编码器测速
选择上拉输入或下拉输入时,应参考外部模块的默认输出电平,保持一致。防止电平冲突。
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
#include "Encoder.h"
//
int16_t Speed;
//
int main(void)
{
//初始化
OLED_Init();
Timer_Init();
Encoder_Init();
OLED_ShowString(1, 1, "Speed:");
while (1)
{
Speed = Encoder_Get(); // 获取编码器的值并存储在Speed变量中
OLED_ShowSignedNum(1, 7, Speed, 5); // 在OLED上显示Speed的值
}
}
//
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
Speed = Encoder_Get(); // 获取编码器的值并存储在Speed变量中
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除定时器2更新中断标志位
}
}
Encoder.c
#include "stm32f10x.h" // 包含STM32标准库的头文件
void Encoder_Init(void)
{
//开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置GPIO引脚模式、速度、和上拉输入
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置TIM3定时器为编码器模式
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
// 配置TIM3的输入捕获通道1和通道2
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter = 0xF;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICFilter = 0xF;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
// 配置编码器接口
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
// 启动TIM3定时器
TIM_Cmd(TIM3, ENABLE);
}
//
int16_t Encoder_Get(void)
{
int16_t Temp;
Temp = TIM_GetCounter(TIM3); // 获取TIM3计数器的值
TIM_SetCounter(TIM3, 0); // 清零TIM3计数器的值
return Temp; // 返回获取到的值
}
ADC
ADC简介
• ADC(Analog-Digital Converter)模拟 - 数字转换器
DAC则是数字 - 模拟转换器,一般可用PWM平替DAC输出电机所需电压。DAC则可用于波形生成。
• ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁
• 12位逐次逼近型ADC,1us转换时间
• 输入电压范围:0~3.3V,转换结果范围:0~4095
• 18个输入通道,可测量16个外部和2个内部信号源
• 规则组(常规使用)和注入组(突发事件)两个转换单元
• 模拟看门狗自动监测输入电压范围
• STM32F103C8T6 ADC资源:ADC1、ADC2,10个外部输入通道
逐次逼近型ADC

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

ADC基本结构

触发控制可选择软件触发或者硬件触发。
输入通道

规则组的四种转换模式
单次转换,非扫描模式

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

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

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

触发控制

数据对齐

一般使用右对齐,可直接得出结果。也可以使用左对齐,然后裁剪数据分辨率。
转换时间
• AD转换的步骤:采样,保持,量化,编码
• STM32 ADC的总转换时间为:
TCONV = 采样时间 + 12.5个ADC周期
• 例如:当ADCCLK=14MHz,采样时间为1.5个ADC周期
TCONV = 1.5 + 12.5 = 14个ADC周期 = 1μs
校准
• ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差
• 建议在每次上电后执行一次校准
• 启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期
硬件电路

示例程序
一般步骤
1.开启RCC时钟,ADC,GPIO。
2.配置GPIO为模拟输入。
3.配置多路开关,将所要转换的通道接入规则组。
4.配置ADC转换器
5.调用ADC_cmd()函数使能ADC。
AD单通道
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
//
uint16_t ADValue; // 存储AD转换后的数值
float Voltage; // 存储计算后的电压值
//
int main(void)
{
// 初始化
OLED_Init();
AD_Init();
OLED_ShowString(1, 1, "ADValue:");
OLED_ShowString(2, 1, "Volatge:0.00V");
while (1)
{
ADValue = AD_GetValue(); // 获取AD转换后的数值
Voltage = (float)ADValue / 4095 * 3.3; // 计算电压值
// 在OLED上显示AD值和电压值(带小数点)
OLED_ShowNum(1, 9, ADValue, 4);
OLED_ShowNum(2, 9, Voltage, 1);
// 在OLED上显示电压值的小数部分(两位小数)
OLED_ShowNum(2, 11, (uint16_t)(Voltage * 100) % 100, 2);
Delay_ms(100); // 延时100毫秒
}
}
AD.c
#include "stm32f10x.h"
void AD_Init(void)
{
// 使能ADC1和GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置ADC时钟分频
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
// 配置GPIO
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 配置引脚为模拟输入模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置ADC的通道、采样时间
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
//利用结构体配置ADC
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 不使用外部触发
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 单次转换
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 非扫描模式
ADC_InitStructure.ADC_NbrOfChannel = 1; // 转换通道数为1
ADC_Init(ADC1, &ADC_InitStructure);
// 启用ADC1
ADC_Cmd(ADC1, ENABLE);
//校准
ADC_ResetCalibration(ADC1); // 复位校准寄存器
while (ADC_GetResetCalibrationStatus(ADC1) == SET); // 等待校准寄存器复位完成
ADC_StartCalibration(ADC1); // 启动ADC1校准
while (ADC_GetCalibrationStatus(ADC1) == SET); // 等待校准完成
}
//转换函数
uint16_t AD_GetValue(void)
{
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 启动ADC转换
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等待转换完成
return ADC_GetConversionValue(ADC1); // 获取ADC转换结果
}
AD多通道
采用扫描模式实现多通道,最好配合DMA防止数据覆盖。
本次利用单次转换非扫描模式,只需在每次触发转换之前,手动更改一下列表第一个位置通道即可。
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
// 存储四个ADC通道的转换值
uint16_t AD0, AD1, AD2, AD3;
int main(void)
{
// 初始化
OLED_Init();
AD_Init();
OLED_ShowString(1, 1, "AD0:");
OLED_ShowString(2, 1, "AD1:");
OLED_ShowString(3, 1, "AD2:");
OLED_ShowString(4, 1, "AD3:");
while (1)
{
// 获取ADC通道的转换值
AD0 = AD_GetValue(ADC_Channel_0);
AD1 = AD_GetValue(ADC_Channel_1);
AD2 = AD_GetValue(ADC_Channel_2);
AD3 = AD_GetValue(ADC_Channel_3);
// 在OLED上显示
OLED_ShowNum(1, 5, AD0, 4);
OLED_ShowNum(2, 5, AD1, 4);
OLED_ShowNum(3, 5, AD2, 4);
OLED_ShowNum(4, 5, AD3, 4);
// 延时100毫秒
Delay_ms(100);
}
}
AD.c
#include "stm32f10x.h"
void AD_Init(void)
{
// 使能ADC1和GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置ADC时钟分频
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
// 配置GPIO引脚为模拟输入模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置ADC的通道、采样时间等参数
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 不使用外部触发
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 单次转换
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 非扫描模式
ADC_InitStructure.ADC_NbrOfChannel = 1; // 转换通道数为1
ADC_Init(ADC1, &ADC_InitStructure);
// 启用ADC1
ADC_Cmd(ADC1, ENABLE);
//校准
ADC_ResetCalibration(ADC1); // 复位校准寄存器
while (ADC_GetResetCalibrationStatus(ADC1) == SET); // 等待校准寄存器复位完成
ADC_StartCalibration(ADC1); // 启动ADC1校准
while (ADC_GetCalibrationStatus(ADC1) == SET); // 等待校准完成
}
//
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5); // 配置ADC
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 启动ADC转换
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等待转换完成
return ADC_GetConversionValue(ADC1); // 获取ADC转换结果
}
DMA
DMA简介
• DMA(Direct Memory Access)直接存储器存取
• DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源(可直接访问32内部存储器,包括内存SRAM,Flash)
• 12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)
• 每个通道都支持软件触发和特定的硬件触发
• STM32F103C8T6 DMA资源:DMA1(7个通道)
存储器映像

DMA框图

DMA基本结构

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

使用硬件触发需选择专门的通道,选择软件触发则可任意选择。
数据宽度与对齐

源端宽度=目标宽度,数据正常传输。
源端宽度<目标宽度,目标前面空位补零。
源端宽度>目标宽度,舍弃源端高位。
数据转运+DMA

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

示例程序
DMA数据转运
main.c
#include "stm32f10x.h" // 包含STM32标准库的头文件
#include "Delay.h" // 包含自定义延时函数的头文件
#include "OLED.h" // 包含OLED显示屏的头文件
#include "MyDMA.h" // 包含自定义DMA初始化和传输函数的头文件
// 定义两个数组,DataA和DataB,分别用于存储数据
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
uint8_t DataB[] = {0, 0, 0, 0};
int main(void)
{
// 初始化OLED显示屏
OLED_Init();
// 使用自定义的DMA初始化函数初始化DMA,并配置数据源和目标地址
MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);
// 在OLED上显示文本和数据的初始状态
OLED_ShowString(1, 1, "DataA");
OLED_ShowString(3, 1, "DataB");
OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);
OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);
while (1)
{
// 修改DataA数组的值
DataA[0]++;
DataA[1]++;
DataA[2]++;
DataA[3]++;
// 在OLED上显示DataA和DataB的各个元素的值
OLED_ShowHexNum(2, 1, DataA[0], 2);
OLED_ShowHexNum(2, 4, DataA[1], 2);
OLED_ShowHexNum(2, 7, DataA[2], 2);
OLED_ShowHexNum(2, 10, DataA[3], 2);
OLED_ShowHexNum(4, 1, DataB[0], 2);
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
// 使用自定义的延时函数延时1秒
Delay_ms(1000);
// 使用DMA传输数据
MyDMA_Transfer();
// 再次在OLED上显示DataA和DataB的各个元素的值
OLED_ShowHexNum(2, 1, DataA[0], 2);
OLED_ShowHexNum(2, 4, DataA[1], 2);
OLED_ShowHexNum(2, 7, DataA[2], 2);
OLED_ShowHexNum(2, 10, DataA[3], 2);
OLED_ShowHexNum(4, 1, DataB[0], 2);
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
// 使用自定义的延时函数延时1秒
Delay_ms(1000);
}
}
MyDMA.c
#include "stm32f10x.h"
// 定义一个全局变量,用于存储DMA传输的数据大小
uint16_t MyDMA_Size;
// DMA初始化函数,用于配置DMA通道
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
MyDMA_Size = Size; // 将传入的数据大小保存到全局变量中
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 启用DMA1时钟
//结构体配置DMA
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA; // 配置外设基地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 配置外设数据大小为字节
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; // 允许外设地址递增
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB; // 配置内存基地址
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 配置内存数据大小为字节
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 允许内存地址递增
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 数据传输方向:从外设到内存
DMA_InitStructure.DMA_BufferSize = Size; // 配置DMA传输的数据大小
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // DMA工作模式为普通模式
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; // 启用内存到内存传输
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; // 配置DMA优先级为中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); // 使用通道1初始化DMA
// 禁用DMA通道1,等待启动传输
DMA_Cmd(DMA1_Channel1, DISABLE);
}
// DMA传输函数,用于启动DMA传输
void MyDMA_Transfer(void)
{
DMA_Cmd(DMA1_Channel1, DISABLE); // 禁用DMA通道1,准备重新配置
DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size); // 配置当前数据传输计数器的值
DMA_Cmd(DMA1_Channel1, ENABLE); // 启用DMA通道1,开始传输
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); // 等待传输完成,检查传输完成标志
DMA_ClearFlag(DMA1_FLAG_TC1); // 清除传输完成标志
}
DMA+AD多通道
main.c
#include "stm32f10x.h"
#include "AD.h"
#include "OLED.h"
#include "Delay.h"
int main(void)
{
// 初始化
OLED_Init();
AD_Init();
OLED_ShowString(1, 1, "AD0:");
OLED_ShowString(2, 1, "AD1:");
OLED_ShowString(3, 1, "AD2:");
OLED_ShowString(4, 1, "AD3:");
while (1)
{
// 读取ADC模块的四个通道(AD0、AD1、AD2、AD3)的值,并显示在OLED屏上
OLED_ShowNum(1, 5, AD_Value[0], 4);
OLED_ShowNum(2, 5, AD_Value[1], 4);
OLED_ShowNum(3, 5, AD_Value[2], 4);
OLED_ShowNum(4, 5, AD_Value[3], 4);
// 延时100毫秒
Delay_ms(100);
}
}
AD.c
#include "stm32f10x.h"
// 用于存储ADC模块的转换结果
uint16_t AD_Value[4];
void AD_Init(void)
{
// 启用ADC1、GPIOA和DMA1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置ADC时钟分频
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
// 配置ADC通道对应的GPIO引脚为模拟输入模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置ADC通道和采样时间
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);
// 配置ADC工作模式和DMA模式
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_InitStructure.ADC_ScanConvMode = ENABLE;
ADC_InitStructure.ADC_NbrOfChannel = 4;
ADC_Init(ADC1, &ADC_InitStructure);
// 配置DMA通道,用于传输ADC数据
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = 4;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
// 启用DMA通道
DMA_Cmd(DMA1_Channel1, ENABLE);
// 启用ADC的DMA传输
ADC_DMACmd(ADC1, ENABLE);
// 启用ADC
ADC_Cmd(ADC1, ENABLE);
// 复位ADC校准
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
// 开始ADC校准
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
// 启动ADC转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
USART串口
通信接口
• 通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
• 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发

引脚:
TX:数据发送引脚
RX:数据接收引脚
SCL:时钟线
SDA:数据线
SCLK:时钟
MOSI:主机输出数据脚
MISO:主机输入数据脚
CS:片选,用于指定通信对象
CAN和USB:表中的双引脚用于表示差分信号。
双工:
全双工:双方可同时互相发送和接受信息。
半双工:任意一方发送的时候,另一方只能接收。
单工:只能一方发送,另一方接收,单向传输。
时钟:
同步:双方通信配有时钟线
异步:自定义时钟
单端:引脚的高低电平为GND的电压差,所以双方需要共地。
硬件电路
• 简单双向串口通信有两根通信线(发送端TX和接收端RX)
• TX与RX要交叉连接
• 当只需单向的数据传输时,可以只接一根通信线
• 当电平标准不一致时,需要加电平转换芯片

电平标准
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
• TTL电平:+3.3V或+5V表示1,0V表示0
• RS232电平:-3 ~ -15V表示1,+3 ~ +15V表示0
• RS485电平:两线压差+2 ~ +6V表示1,-2 ~ -6V表示0(差分信号)
串口参数及时序
• 波特率:串口通信的速率
• 起始位:标志一个数据帧的开始,固定为低电平
• 数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行
• 校验位:用于数据验证,根据数据位计算得来
• 停止位:用于数据帧间隔,固定为高电平

右图RB8/TB8为奇偶校验位。
串口时序

各种情况下,串口发送数据的时序。
USART外设简介
• USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器
• USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里
• 自带波特率发生器,最高达4.5Mbits/s(可理解为分频器)
• 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)
• 可选校验位(无校验/奇校验/偶校验)
• 支持同步模式(CLK时钟输出)、硬件流控制、DMA、智能卡、IrDA、LIN
• STM32F103C8T6 USART资源: USART1、 USART2、 USART3
USART框图

USART基本结构

数据帧


起始位侦测

数据采样

波特率发生器
• 发送器和接收器的波特率由波特率寄存器BRR里的DIV确定
• 计算公式:波特率 = fPCLK2/1 / (16 * DIV)

数据模式
• HEX模式/十六进制模式/二进制模式:以原始数据的形式显示
• 文本模式/字符模式:以原始数据编码后的形式显示


示例程序
串口发送/接收数据
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
//定义接收数据
uint8_t RxData;
int main(void)
{
//初始化数据
OLED_Init();
OLED_ShowString(1, 1, "RxData:");
Serial_Init();
while (1)
{
//实现功能为数据互传,当上位机发送数据,从机接收到数据时,将收到的数据回传发送。
if (Serial_GetRxFlag() == 1)
{
RxData = Serial_GetRxData();
Serial_SendByte(RxData);
OLED_ShowHexNum(1, 8, RxData, 2);
}
}
}
Serial.c
#include "stm32f10x.h" // Device header
#include
#include
//定义临时变量
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;
//初始化函数
void Serial_Init(void)
{
//开启GPIO和USART的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//初始化GPIO
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//用于接收
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置USART
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600; //配置波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //不使用硬件流控制
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //配置USART为发送和接收模式
USART_InitStructure.USART_Parity = USART_Parity_No; //配置校验位,无校验
USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位为1位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //子长为8字节
USART_Init(USART1, &USART_InitStructure);
//开启USART中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
//用NVIC进行分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//配置中断
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
//使能USART
USART_Cmd(USART1, ENABLE);
}
//发送一个字节,调用标准库函数发送字节,并判断标志位等待发送。
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
//发送一个数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Array[i]);
}
}
//发送字符串
void Serial_SendString(char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i ++)
{
Serial_SendByte(String[i]);
}
}
//求平方
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y --)
{
Result *= X;
}
return Result;
}
//发送数字
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
}
}
//printf调用fputc打印,此函数使fputc为发送到串口。
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
//printf重定向,用于在程序中,调用printf,可打印数据至串口
void Serial_Printf(char *format, ...)
{
char String[100];
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);
Serial_SendString(String);
}
//自定义函数,获取读标志位
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
//自定义函数,获取读标志位
uint8_t Serial_GetRxData(void)
{
return Serial_RxData;
}
//USART中断,读取接收数据
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
Serial_RxData = USART_ReceiveData(USART1);
Serial_RxFlag = 1;
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
具体函数功能已有注释,值得一提的是发送数字的功能,除了示例程序中,求数字次方再提取数字(此方法在博客C语言循环结构中的题目《计数问题》有更好的处理)。也可以利用发送字符串的功能。利用C语言函数,可将数字转换为字符串再发送。当然,别忘了引入C语言头文件<stdio.h>。
1 | char VoltageStr[10]; // 定义一个足够大的字符数组来存储转换后的字符串 |
数据包
HEX数据包

文本数据包

HEX数据包接收

文本数据包接收

在程序设计中,需设计一个能记住不同状态的机制,称为状态机。
示例程序
串口收发HEX数据包
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "Key.h"
// 用于存储按键的状态
uint8_t KeyNum;
int main(void)
{
//初始化
OLED_Init();
Key_Init();
Serial_Init();
OLED_ShowString(1, 1, "TxPacket");
OLED_ShowString(3, 1, "RxPacket");
// 预设串口发送数据的内容
Serial_TxPacket[0] = 0x01;
Serial_TxPacket[1] = 0x02;
Serial_TxPacket[2] = 0x03;
Serial_TxPacket[3] = 0x04;
while (1)
{
//获取按键状态
KeyNum = Key_GetNum();
// 如果按键被按下,将递增串口发送数据的内容
if (KeyNum == 1)
{
Serial_TxPacket[0]++;
Serial_TxPacket[1]++;
Serial_TxPacket[2]++;
Serial_TxPacket[3]++;
// 发送串口数据
Serial_SendPacket();
// 在OLED上显示发送数据的十六进制值
OLED_ShowHexNum(2, 1, Serial_TxPacket[0], 2);
OLED_ShowHexNum(2, 4, Serial_TxPacket[1], 2);
OLED_ShowHexNum(2, 7, Serial_TxPacket[2], 2);
OLED_ShowHexNum(2, 10, Serial_TxPacket[3], 2);
}
// 如果收到串口接收标志位
if (Serial_GetRxFlag() == 1)
{
// 在OLED上显示接收到的数据的十六进制值
OLED_ShowHexNum(4, 1, Serial_RxPacket[0], 2);
OLED_ShowHexNum(4, 4, Serial_RxPacket[1], 2);
OLED_ShowHexNum(4, 7, Serial_RxPacket[2], 2);
OLED_ShowHexNum(4, 10, Serial_RxPacket[3], 2);
}
}
}
Serial.c
#include "stm32f10x.h" // 包含STM32的设备头文件
#include
#include
//定义数据
uint8_t Serial_TxPacket[4]; // 用于存储串口发送的数据
uint8_t Serial_RxPacket[4]; // 用于存储串口接收的数据
uint8_t Serial_RxFlag; // 串口接收标志位
// 初始化串口
void Serial_Init(void)
{
// 使能USART1和GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置GPIO发送端口
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // USART1的TX引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置GPIO接收端口
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // USART1的RX引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置USART
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600; // 波特率设置为9600
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
// 使能USART1的接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 设置NVIC的优先级分组为组2
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 配置USART1的中断
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
// 使能USART1
USART_Cmd(USART1, ENABLE);
}
// 发送单个字节
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
// 发送字节数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Array[i]);
}
}
// 发送字符串
void Serial_SendString(char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i ++)
{
Serial_SendByte(String[i]);
}
}
// 计算X的Y次方
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y --)
{
Result *= X;
}
return Result;
}
// 发送数字
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
}
}
// 重定向printf函数
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
// 带格式的字符串发送
void Serial_Printf(char *format, ...)
{
char String[100];
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);
Serial_SendString(String);
}
// 发送串口数据包
void Serial_SendPacket(void)
{
Serial_SendByte(0xFF); //数据包包头
Serial_SendArray(Serial_TxPacket, 4); //数据包数据
Serial_SendByte(0xFE); //数据包包尾
}
// 获取串口接收标志位
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
// USART1的中断处理函数,内有状态机,处理数据包各种情况
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0;
static uint8_t pRxPacket = 0;
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
uint8_t RxData = USART_ReceiveData(USART1);
if (RxState == 0)
{
if (RxData == 0xFF)
{
RxState = 1;
pRxPacket = 0;
}
}
else if (RxState == 1)
{
Serial_RxPacket[pRxPacket] = RxData;
pRxPacket ++;
if (pRxPacket >= 4)
{
RxState = 2;
}
}
else if (RxState == 2)
{
if (RxData == 0xFE)
{
RxState = 0;
Serial_RxFlag = 1;
}
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
Serial.h
#ifndef __SERIAL_H
#define __SERIAL_H
//
#include
//定义变量时用extern,可被主函数调用。
extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];
//
void Serial_Init(void); //初始化
void Serial_SendByte(uint8_t Byte); //发送字节
void Serial_SendArray(uint8_t *Array, uint16_t Length); //发送数组
void Serial_SendString(char *String); //发送字符串
void Serial_SendNumber(uint32_t Number, uint8_t Length); //发送数字
void Serial_Printf(char *format, ...); //打印至串口
void Serial_SendPacket(void); //发送数据包
uint8_t Serial_GetRxFlag(void); //获取接收标志位
//
#endif
这里引入.h文件是因为定义到Sericl.c的变量,需要在main.c调用。
各函数功能和串口收发数据区别不大,但在中断函数中引入的状态机,先等待包头,收到包头后开启接受数据的函数,并保存至数组。接受完毕后等待包尾,完成一个数据包的传输。
主函数的功能是按下按钮时,将发送以“FF 02 03 04 05 FE”为格式的数据包,按键每按下一次,数据包内的数据将自增1。也可以用同样的格式发送数据包,发送和接受的数据包的数据(不含包头包尾)将在OLED显示。
串口收发文本数据包
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "LED.h"
#include "string.h"
int main(void)
{
//初始化
OLED_Init();
LED_Init();
Serial_Init();
OLED_ShowString(1, 1, "TxPacket");
OLED_ShowString(3, 1, "RxPacket");
while (1)
{
if (Serial_RxFlag == 1) // 如果收到串口接收标志位
{
OLED_ShowString(4, 1, " "); // 清除OLED上的数据
OLED_ShowString(4, 1, Serial_RxPacket); // 在OLED上显示收到的数据
if (strcmp(Serial_RxPacket, "LED_ON") == 0) // 如果收到 "LED_ON" 命令
{
LED1_ON(); // 打开LED1
Serial_SendString("LED_ON_OK\r\n"); // 发送成功消息给串口
OLED_ShowString(2, 1, " "); // 清除OLED上的数据
OLED_ShowString(2, 1, "LED_ON_OK"); // 在OLED上显示 "LED_ON_OK"
}
else if (strcmp(Serial_RxPacket, "LED_OFF") == 0) // 如果收到 "LED_OFF" 命令
{
LED1_OFF(); // 关闭LED1
Serial_SendString("LED_OFF_OK\r\n"); // 发送成功消息给串口
OLED_ShowString(2, 1, " "); // 清除OLED上的数据
OLED_ShowString(2, 1, "LED_OFF_OK"); // 在OLED上显示 "LED_OFF_OK"
}
else
{
Serial_SendString("ERROR_COMMAND\r\n"); // 发送错误消息给串口
OLED_ShowString(2, 1, " "); // 清除OLED上的数据
OLED_ShowString(2, 1, "ERROR_COMMAND"); // 在OLED上显示 "ERROR_COMMAND"
}
Serial_RxFlag = 0; // 重置串口接收标志位
}
}
}
Serial.c
#include "stm32f10x.h"
#include
#include
//
char Serial_RxPacket[100]; // 存储串口接收的数据,最大长度为100
uint8_t Serial_RxFlag; // 串口接收标志位
// 初始化串口
void Serial_Init(void)
{
// 使能USART1和GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置GPIO发送端
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // USART1的TX引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置GPIO接收端
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // USART1的RX引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置USART
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600; // 波特率设置为9600
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
// 使能USART1的接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 设置NVIC的优先级分组为组2
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 配置USART1的中断
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
// 使能USART1
USART_Cmd(USART1, ENABLE);
}
// 发送单个字节
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
// 发送字节数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Array[i]);
}
}
// 发送字符串
void Serial_SendString(char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i ++)
{
Serial_SendByte(String[i]);
}
}
// 计算X的Y次方
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y --)
{
Result *= X;
}
return Result;
}
// 发送数字
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
}
}
// 重定向printf函数
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
// 带格式的字符串发送
void Serial_Printf(char *format, ...)
{
char String[100];
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);
Serial_SendString(String);
}
// USART1的中断处理函数,内有状态机,处理数据包各种情况
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0;
static uint8_t pRxPacket = 0;
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
uint8_t RxData = USART_ReceiveData(USART1);
if (RxState == 0)
{
if (RxData == '@' && Serial_RxFlag == 0) // 如果接收到'@'字符且标志位为0
{
RxState = 1;
pRxPacket = 0;
}
}
else if (RxState == 1)
{
if (RxData == '\r') // 如果接收到'\r'字符
{
RxState = 2;
}
else
{
Serial_RxPacket[pRxPacket] = RxData; // 存储接收到的字符
pRxPacket ++;
}
}
else if (RxState == 2)
{
if (RxData == '\n') // 如果接收到'\n'字符
{
RxState = 0;
Serial_RxPacket[pRxPacket] = '\0'; // 在末尾添加字符串结束符
Serial_RxFlag = 1; // 设置串口接收标志位为1,表示接收完成
}
}
//清除标志位
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
Serial.h
#ifndef __SERIAL_H
#define __SERIAL_H
#include
// 定义串口接收缓冲区和接收标志位,这些变量可在main中引用
extern char Serial_RxPacket[];
extern uint8_t Serial_RxFlag;
//
void Serial_Init(void); // 串口初始化函数
void Serial_SendByte(uint8_t Byte); // 发送单个字节到串口
void Serial_SendArray(uint8_t *Array, uint16_t Length); // 发送字节数组到串口
void Serial_SendString(char *String); // 发送字符串到串口
void Serial_SendNumber(uint32_t Number, uint8_t Length); // 发送一个数字到串口,Length 表示数字的位数
void Serial_Printf(char *format, ...); // 带格式的字符串发送,类似于 printf
//
#endif
和HEX数据包类似,只是将数据形式换成了字符型。就不做过多说明。
main函数实现的功能为,在上位机输入‘@’为包头,输入‘LED_ON’为数据,换行为包尾,发送后将点亮LED,并回传“LED_ON_OK”,输入‘LED_OFF’为数据,发送后将熄灭LED,并回传“LED_OFF_OK”。输入其它指令将无法控制LED,并收到回传“ERROE_COMMAND”。
FlyMcu程序烧录
串口下载原理


写入的代码通过软件生成.hex文件后,配置BOOT引脚,使STM32执行BootLoader程序,设置BOOT0 = 1并按下复位键,等程序下载完,此时仍在执行下载程序,所以需置BOOT0 = 0,再次按下复位键。
如果stm32没有设置读保护,则写入的程序可被软件读出来,当然,一般只能是hex一类,反编译源码还是不现实。