目录
[1 时钟(心跳)](#1 时钟(心跳))
[1.1 CubeMX基本配置](#1.1 CubeMX基本配置)
[1.2 外设在时钟上的分配原理](#1.2 外设在时钟上的分配原理)
[1.3 时钟树](#1.3 时钟树)
[2 寄存器(地址)](#2 寄存器(地址))
[3 GPIO](#3 GPIO)
[3.1 GPIO实物](#3.1 GPIO实物)
[3.2 GPIO两种结构(推挽/开漏)](#3.2 GPIO两种结构(推挽/开漏))
[3.3 LED](#3.3 LED)
[3.4 CUBEMX](#3.4 CUBEMX)
[3.5 常用函数](#3.5 常用函数)
[3.6 实验1:控制LED交替闪烁](#3.6 实验1:控制LED交替闪烁)
[3.7 实验2:按键控制LED的亮灭(1)](#3.7 实验2:按键控制LED的亮灭(1))
[3.8 实验2:按键控制LED的亮灭(2)](#3.8 实验2:按键控制LED的亮灭(2))
[4 中断](#4 中断)
[4.1 中断优先级](#4.1 中断优先级)
[4.2 实验3:利用外部中断控制LED亮灭(1)(借助其他引脚)](#4.2 实验3:利用外部中断控制LED亮灭(1)(借助其他引脚))
[4.3 实验3:利用外部中断控制LED亮灭(2)(按键引脚)](#4.3 实验3:利用外部中断控制LED亮灭(2)(按键引脚))
[5 UART/USART](#5 UART/USART)
[5.1 通讯分类](#5.1 通讯分类)
[5.2 USART](#5.2 USART)
[5.3 常用函数](#5.3 常用函数)
[5.4 实验4:利用蓝牙模块实现串口通信](#5.4 实验4:利用蓝牙模块实现串口通信)
[5.5 printf/scanf的重定向](#5.5 printf/scanf的重定向)
[6 DMA](#6 DMA)
[6.1 特性及原理](#6.1 特性及原理)
[6.2 FIFO](#6.2 FIFO)
[6.3 传输、工作模式](#6.3 传输、工作模式)
[6.3.1 传输模式:单次传输与突发传输](#6.3.1 传输模式:单次传输与突发传输)
[6.3.2 工作模式:FIFO模式与直接模式](#6.3.2 工作模式:FIFO模式与直接模式)
[6.4 实验5:利用空闲中断+串口DMA实现不定长收发](#6.4 实验5:利用空闲中断+串口DMA实现不定长收发)
[7 HAL库外设驱动原理](#7 HAL库外设驱动原理)
[7.1 句柄结构体(xx_HandleTypeDef)](#7.1 句柄结构体(xx_HandleTypeDef))
[7.2 初始化结构体(xx_InitTypeDef)](#7.2 初始化结构体(xx_InitTypeDef))
[8 定时器](#8 定时器)
[8.1 基础知识](#8.1 基础知识)
[8.2 PWM](#8.2 PWM)
[8.3 相关函数](#8.3 相关函数)
[8.3.1 定时器基本函数](#8.3.1 定时器基本函数)
[8.3.2 PWM](#8.3.2 PWM)
[8.4 实验6:定时器输入捕获实验](#8.4 实验6:定时器输入捕获实验)
[8.5 实验7:舵机驱动](#8.5 实验7:舵机驱动)
[9 I2C](#9 I2C)
[9.1 简介](#9.1 简介)
[10 SPI](#10 SPI)
[10.1 简介](#10.1 简介)
1 时钟(心跳)
1.1 CubeMX基本配置
不同的时钟源,通过分频或者倍频处理后送到相应的外设单元。
实际应用中根据需要配置外设的时钟控制开关,选择需要的时钟频率,并可关闭不用外设时钟。
这里以STM32F407ZG为例
1.首先根据自己的芯片类型来选择MCU(Microcontroller Unit,微控制器单元)
2.SYS 选项卡勾选Serial Wire(不勾选可能会使得无法使用stlink或jlink无法下载)
3.在RCC选项卡中将HSE设置为晶振
4.清板子上晶振原件显示的频率是多少
1.2 外设在时钟上的分配原理
外设以设定的组别挂在0x0000 0000到0xFFFF FFFF的内存区域中
以APB1为例,其所附带的外设如下
1.3 时钟树
1、HSI:高速内部时钟信号 stm32单片机内带的时钟 (8M频率)精度较差
2、HSE:高速外部时钟信号精度高来源(1)HSE外部晶体/陶瓷谐振器(晶振) (2)HSE用户外部时钟
3、LSE:低速外部晶体 32.768kHz 主要提供一个精确的时钟源 一般作为RTC时钟使用
五大时钟源
LSE:低速外部晶体 32.768kHz 主要提供一个精确的时钟源 一般作为RTC时钟使用
在STM32中,有五个时钟源,为HSI、HSE、LSI、LSE、PLL:
①、HSI是高速内部时钟,RC振荡器,频率为8MHz。
②、HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~16MHz。
③、LSI是低速内部时钟,RC振荡器,频率为40kHz。
④、LSE是低速外部时钟,接频率为32.768kHz的石英晶体。
⑤、PLL为锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。倍频可选择为2~16倍,但是其输出频率最大不得超过72MHz。
2 寄存器(地址)
cpp
//寄存器编程
int main()
{
volatile unsigned int* pointer=(unsigned int*)0x40028000;//volatile的作用是防止变量被编译器优化
*pointer+=1;
*pointer+=2; //优化后*pointer+=3 对于寄存器来说先后加1和2和直接加3是不一样的
}
cpp
#define pointer (volatile unsigned int*)0x40028000 //预处理不占用内存
int main()
{
*pointer+=1;
*pointer+=2; //优化后*pointer+=3 对于寄存器来说先后加1和2和直接加3是不一样的
}
3 GPIO
GPIO(general porpose intput output):通用输入输出端口的简称。可以通过软件控制其输出和输入。stm32芯片的GPIO引脚与外部设备连接起来,从而实现与外部通信,控制以及数据采集的功能。
3.1 GPIO实物
3.2 GPIO两种结构(推挽/开漏)
推挽输出:当IN是高电平时,上边的三极管处于导通状态,下边的三极管处于断开状态,OUT与VCC相连;当IN是低电平时,上边的三极管处于断开状态,下边的三极管处于导通状态,OUT与GND相连
开漏输出:OUT默认与VCC相连,当IN是高电平时,三极管处于断开状态,OUT与VCC相连;当IN是低电平时,三极管处于导通状态,OUT与GND相连
由此可见:推挽输出的控制能力更强,开漏输出有线与的特点(
两个或两个以上与非门的输出端连接在同一条导线上,将这些与非门上的数据(状态电平)用同一条导线输送出去。
)
3.3 LED
cpp
//基地址+偏移地址
#define GPIOB_CLK (*(volatile unsigned int*)(0x40021000+0x18))
#define GPIOB_CRL (*(volatile unsigned int*)(0x40010c00+0x00))
#define GPIOB_ODR (*(volatile unsigned int*)(0x40010c00+0x0c))
int main()
{
//1.使能GPIOB的外设时钟
GPIOB_CLK|=(1<<3);
//2.GPIOB配置推挽输出模式
GPIOB_CRL&= ~(0xf<<(4*0));//清除低4位寄存器
GPIOB_CRL|=(2<<0);
GPIOB_ODR&= ~(0x1<<(1*0));//清除低1位寄存器
GPIOB_ODR|=(1<<0);
}
清除位的步骤解释:0xf是1111
按位取反:
3.4 CUBEMX
configuration:配置
GPIO output level: 输出电平
GPIO mode:输出模式(推挽输出/开漏输出)
GPIO Pull-up/Pull-down:上拉/下拉
Maximum output speed:最大输出速度
User Label:用户定义标签
这里说一下GPIO上拉下拉:
GPIO通常有三种状态:高电平、低电平和高阻态。高阻态换句话说就是断开状态或浮空态。因此上拉和下拉其中一个强大的理由就是为了防止输入端悬空,使其有确定的状态。减弱外部电流对芯片的产生的干扰。
上拉就是将不确定的信号通过一个电阻提升为高电平,这个上拉的电阻的选择通常有讲究,通常是驱动能力和功耗的平衡,若GPIO为输出为高电平,一般来说,上拉电阻越小,驱动能力越强,但功耗也就越大,同时还要考虑下级电路对驱动能力的要求,上拉电阻选择的合适才能向下级电路提供足够的电流。另外就是数字电路对高低电平都有一个门槛,以上拉电阻为例,输出高电平自然是被拉高,但输出低电平的时候,通常内部的开关管会被导通到地,这必须确保内部导通到地这一段之间的电阻和上拉电阻的比值足够让其电平处在零电平门槛之下。对频率比较高的时候,上拉电阻和开关管漏源级之间的电容和下级电路之间的输入电容会形成"RC延迟",电阻越大,延迟越大。下拉电阻的选择原理和上拉电阻是一样的。
3.5 常用函数
cpp
HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);//初始化引脚
HAL_GPIO_DeInit(GPIOA,GPIO_PIN_1);//初始化PA1引脚为复位状态
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_SET);//将PA1置高电平
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_RESET);//将PA1置低电平
HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1);//读取PA1电平状态
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_1);//取反PA1电平
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_1);//调用中断回调函数
HAL_GPIO_EXTI_Callback(GPIO_PIN_1);//中断函数具体要响应的动作
HAL_GPIO_LockPin(GPIOA,GPIO_PIN_1);//锁定PA1电平状态
3.6 实验1:控制LED交替闪烁
第一步:基础配置(RCC+时钟树)
第二步:参照数据手册,找到LED对应的引脚,并开启GPIO输出功能:
第三步:生成代码
第四步:写程序
cpp
while (1)
{
/* USER CODE END WHILE */
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_SET);//高电平时灭
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_RESET);//低电平时亮
HAL_Delay(1000);//延时函数
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_RESET);//低电平时亮
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_SET);//高电平时灭
HAL_Delay(1000);//延时函数
/* USER CODE BEGIN 3 */
}
第五步:编译+写入
3.7 实验2:按键控制LED的亮灭(1)
第一步:基础配置(RCC+时钟树)
第二步:参照数据手册,找到LED和按键(KEY0,KEY1)对应的引脚,并开启GPIO输出/输入功能:
第三步:设置上拉输入
上拉输入相当于接一个VCC端,如果按键没有按下,那么IN端与VCC端相连,是高电平
如果按键按下,那么IN端与GND端相连,是低电平
第四步:生成代码
第五步:写程序
cpp
while (1)
{
/* USER CODE END WHILE */
if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_3)==GPIO_PIN_RESET)
{
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_SET);
}
if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_4)==GPIO_PIN_RESET)
{
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_RESET);
}
/* USER CODE BEGIN 3 */
}
第六步:编译+写入
3.8 实验2:按键控制LED的亮灭(2)
第一步:基础配置(RCC+时钟树)
第二步:参照数据手册,找到LED和按键(KEY0,KEY1,WK_UP)对应的引脚,并开启GPIO输出/输入功能:
第三步:设置上拉/下拉输入
同理,由于WK_UP按键接VCC,所以配置下拉输入
第四步:生成代码
第五步:写程序
cpp
while (1)
{
/* USER CODE END WHILE */
if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_3)==GPIO_PIN_RESET)
{
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_SET);
}
if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_4)==GPIO_PIN_RESET)
{
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_RESET);
}
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==GPIO_PIN_SET)
{
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_RESET);
}
/* USER CODE BEGIN 3 */
}
第六步:编译+写入
4 中断
中断是指CPU在执行当前程序时系统出现了某种状况,使得CPU必须停止当前程序,而去执行另一段程序来处理的出现的紧急事务,处理结束后CPU再返回到原先暂停的程序继续执行,这个过程就称为中断。
4.1 中断优先级
(1)中断优先级分为两种,可编程和不可编程,可编程的表示可以自己修改中断优先级,不可编程的就不能修改
(2)对于STM32中断优先级,决定着内核优先响应谁的中断请求
(3)小值优先原则,中断优先级数值越小,中断就会被优先相应
(4)中断优先级按照优先级分组配置
STM32上只使用M3内核支持的8bit优先级中的高4位bit,也就是STM32支持2^4个优先级
|------|------|------|------|------|------|------|------|
| bit7 | bit6 | bit5 | bit4 | bit3 | bit2 | bit1 | bit0 |
| 用于表达优先级 |||| 未使用,读回为0 ||||
使用这4个bit,组织成5组优先级分组,每组分为1个抢占组,1个子优先级组
优先级分组 抢占优先级 子优先级 描述
NVIC_PriorityGroup_0 0 0-15 主-0bit,子-4bit
NVIC_PriorityGroup_1 0-1 0-7 主-1bit,子-3bit
NVIC_PriorityGroup_2 0-3 0-3 主-2bit,子-2bit
NVIC_PriorityGroup_3 0-7 0-1 主-3bit,子-1bit
NVIC_PriorityGroup_4 0-15 0 主-4bit,子-0bit
对于组0,抢占优先级为0,表示他没有抢占优先级,4个bit全部用来表示子优先级。对于组1,抢占优先级为0-1,用1个bit表示抢占优先级,其余3个bit表示子优先级...
(1)通过优先级分组,可以管理中断的响应顺序
(2)只有抢占优先级才由抢占中断权限,发生中断嵌套,打断就发生中断嵌套,没有能力打断,那就被挂起
假如事件A抢占优先级为0,B的抢占优先级为10,在B执行过程中,A发出中断请求,则会抢过B中断的使用权,等A执行完毕再继续执行B(ps:如果A并不能打断事件B,A就会被挂起)
(3)如果中断抢占优先级相同,不发生抢占
(4)如果多个挂起的中断具有相同的抢占优先级,则子优先级高的先行,如果子优先级相同,则IRQ(通常指外部中断请求)编号小的先行
所以:主优先级>子优先级>IRQ编号
4.2 实验3:利用外部中断控制LED亮灭(1)(借助其他引脚)
第一步:基础配置(下同)
第二步:配置
第三步:生成代码
第四步:写程序
中断回调函数重写(用串口收发检验是否成功进入函数,Delay尽量不要用在中断中,容易卡,这里待改进)
cpp
char buffer1[100]={"第一个中断"};
char buffer2[100]={"第二个中断"};
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)//重写中断回调函数
{
if(GPIO_Pin==GPIO_PIN_1)
{
HAL_UART_Transmit_DMA(&huart1,(unsigned char*)buffer1,100);//检验是否进入函数
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_SET);
HAL_Delay(1000);//延时一秒
HAL_NVIC_SystemReset();//复位函数
}
if(GPIO_Pin==GPIO_PIN_2)
{
HAL_UART_Transmit_DMA(&huart1,(unsigned char*)buffer2,100);
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_SET);
HAL_Delay(1000);
HAL_NVIC_SystemReset();
}
}
中断服务函数
cpp
while (1)
{
/* USER CODE END WHILE */
if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_3)==GPIO_PIN_RESET)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_1);//中断服务函数
}
if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_4)==GPIO_PIN_RESET)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2);
}
/* USER CODE BEGIN 3 */
}
第五步:编译+写入
4.3 实验3:利用外部中断控制LED亮灭(2)(按键引脚)
第一步
第二步 :配置
这里按键仍然选择上拉输入(原因同上)
中断触发模式选择上升沿/下降沿触发
第三步
第四步:写程序
cpp
/* USER CODE BEGIN 0 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_4)==GPIO_PIN_SET)
{
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_SET);
}
if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_4)==GPIO_PIN_RESET)
{
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_RESET);
}
}
/* USER CODE END 0 */
cpp
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_RESET);
HAL_Delay(5000);
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_SET);
HAL_Delay(5000);
}
第五步
5 UART/USART
5.1 通讯分类
比特率:单位时间内传输了多少位(比特),单位是bit/s
波特率:单位时间内传输了多少个符号。
波特率与比特率的关系为:比特率=波特率X单个调制状态对应的二进制位数。
I=S*log2(N)
5.2 USART
CubeMX:
Baud Rate:波特率
Word Length:字长
Parity:校验位
Stop Bits:停止位
串口调试助手:
5.3 常用函数
cpp
HAL_UART_Init(&huart1);//初始化串口1函数
HAL_UART_MspInit(&huart1);//串口1回调函数,主要进行硬件部分的初始化
HAL_UART_Transmit(&huart1, uint8_t *pData, uint16_t Size, uint32_t Timeout);//串口1发送数据函数
HAL_UART_Receive(&huart1, uint8_t *pData, uint16_t Size, uint32_t Timeout);//串口1接收数据函数
HAL_UART_Transmit_IT(&huart1, uint8_t *pData, uint16_t Size);//串口1中断方式发送数据
HAL_UART_Receive_IT(&huart1, uint8_t *pData, uint16_t Size);//串口1中断方式接受数据
HAL_UART_Transmit_DMA(&UART1_Handler, uint8_t *pData, uint16_t Size);//DMA方式发送数据
HAL_UART_Receive_DMA(&UART1_Handler, uint8_t *pData, uint16_t Size);//DMA接收数据
HAL_UART_DMAPause(&UART1_Handler);//暂停DMA数据传输
HAL_UART_DMAResume(&UART1_Handler);//从暂停状态中恢复DMA传送
HAL_UART_DMAStop(&UART1_Handler);//停止DMA的传输
HAL_UART_IRQHandler(&huart1);//串口1中断处理函数
HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);//串口发送 数据完毕的回调函数,当串口使用中断模式发送完毕后才能自动调用本函数。
HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);//发送数据一半回调函数
HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);//串口收发数据出错时的回调函数。
HAL_LIN_SendBreak(UART_HandleTypeDef *huart);//LIN总线通信函数,发送一个断开连接的标识。
HAL_MultiProcessor_EnterMuteMode(UART_HandleTypeDef *huart);//多核处理器进入串口静音模式。
5.4 实验4:利用蓝牙模块实现串口通信
CH340模块(USB转TTL) HC-05模块
VCC(5V) ------------------------------ VCC
GND ------------------------------ GND
RXD ------------------------------ TXD
TXD ------------------------------ RXD
最开始,用USB转TTL连接HC-05模块(注意此时需要先按住HC-05模块上面的按钮再上电 ),这时会进入AT模式(此时的灯应该是间隔比较长时间才灭一次),然后打开串口调试助手,波特率设为38400,接下来进行初始化操作
按照步骤
配置蓝牙模块基本信息
AT+NAME=HC-05 修改蓝牙模块名称为HC-05
AT+ROLE=0 蓝牙模式为从模式
AT+CMODE=1 蓝牙连接模式为任意地址连接模式,也就是说 该模块可以被任意蓝牙设备连接
AT+PSWD=1234 蓝牙配对密码为1234
AT+UART=9600,0,0 蓝牙通信串口波特率为9600,停止位1位, 无校验位
我们直接把蓝牙模块插到原来USB转TTL的位置上(蓝牙模块的用处就是可以无线调试而不用电脑上的虚拟串口调试) (注意不要按按钮)
我们在手机上安装蓝牙串口调试软件,并配对(注意在这之前手机的设置->蓝牙里已经连接成功)
然后这时会发现灯处于长灭状态(偶尔亮一会),就可以正常使用了,我们在手机里输入数据,跟原来用电脑的虚拟串口一模一样
5.5 printf/scanf的重定向
int fputc(int c,FILE *stream)//printf
{
uint8_t ch[1]={c};
HAL_UART_Transmit(&huart1,ch,1,0xFFFF);
return c;
}
int fgetc(FILE *stream)//scanf
{
uint8_t ch[1];
HAL_UART_Receive(&huart1,ch,1,0xFFFF);
return ch[0];
}
6 DMA
6.1 特性及原理
DMA,全称Direct Memory Access,即直接存储器访问。
DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。
DMA定义:
DMA用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU的干预 ,通过DMA数据可以快速地移动。这就节省了CPU的资源来做其他操作。
DMA传输方式
DMA的作用就是实现数据的直接传输,而去掉了传统数据传输需要CPU寄存器参与的环节,主要涉及四种情况的数据传输,但本质上是一样的,都是从内存的某一区域传输到内存的另一区域(外设的数据寄存器本质上就是内存的一个存储单元)。四种情况的数据传输如下:
外设到内存
内存到外设
内存到内存
外设到外设
总结:DMA就是CPU的帮手
首先需要建立传输通道(存储器->存储器、存储器->外设、外设->外设)
其次确定传输对象(UART(源)->内存(目标)、内存(目标)->UART(源))
最后敲定传输细节(确定由谁来产生DMA请求、通道优先级、确定传输双方的数据格式、确定数据是否需要循环采集(循环模式是否使能)、是否需要传输标志/中断)
6.2 FIFO
FIFO(First In First Out,即先入先出),是一种数据缓冲器。先被写入的数据会按顺序先被读出。FIFO可看做一个管道,有数据写入端口和 数据读取端口:
如图,数据写入端口从1~10依次写入数据,则数据读取端口也从1~10依次读取数据。输出端口每读出一位数据,FIFO中的后一位数据就向前移一位。如读取端口读出1、2、3后FIFO输出端口的第一位变为4。
6.3 传输、工作模式
6.3.1 传输模式:单次传输与突发传输
单次传输模式下,一次操作(软件)只能传输一次(一次可以理解为一个节拍,如一个字节)
突发传输模式下,一次操作可以传输多次,如4次,8次,16次(利用到FIFO)
6.3.2 工作模式:FIFO模式与直接模式
FIFO模式下,可以将要传输的多个数据(或字节)累计存储在FIFO缓冲器中,然后在FIFO缓冲器中设置存储阈值,当到达阈值时,FIFO会自动把所有存储的数据一次性的发送到目标地址;
直接模式下,DMA直接进行数据从源地址到目的地址的传输,对于外设的传输,因为外设内部一般也有FIFO,所以传输的数据可以被直接存储在外设的FIFO中。
6.4 实验5:利用空闲中断+串口DMA实现不定长收发
第一步
第二步:配置
第三步
第四步:写程序
while循环外
cpp
HAL_UART_Receive_DMA(&huart1,buffer,3);
__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);
在it.c文件中找到串口中断服务函数
cpp
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE)!=RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
HAL_UART_DMAStop(&huart1);
len=3-__HAL_DMA_GET_COUNTER(huart1.hdmarx);
HAL_UART_Transmit_DMA(&huart1,buffer,len);
HAL_UART_Receive_DMA(&huart1,buffer,3);
}
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
第五步
7 HAL库外设驱动原理
7.1 句柄结构体(xx_HandleTypeDef)
Instance:它指向了外设内,一个具体的外设成员。如:ADC里的ADC1、ADC2,UART里的UART1、UART2,DMA里的Channel1、Channel2,实际上他用指针指向一个外设基地址
Init:指向了一个具体外设的初始化结构体用来配置外设的工作参数
7.2 初始化结构体(xx_InitTypeDef)
8 定时器
8.1 基础知识
递增模式:
递减模式:
中心对齐模式:
8.2 PWM
8.3 相关函数
8.3.1 定时器基本函数
cpp
/* Time Base functions ********************************************************/
HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim);
HAL_StatusTypeDef HAL_TIM_Base_DeInit(TIM_HandleTypeDef *htim);
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim);
void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef *htim);
/* Blocking mode: Polling */
HAL_StatusTypeDef HAL_TIM_Base_Start(TIM_HandleTypeDef *htim);
HAL_StatusTypeDef HAL_TIM_Base_Stop(TIM_HandleTypeDef *htim);
/* Non-Blocking mode: Interrupt */
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);
HAL_StatusTypeDef HAL_TIM_Base_Stop_IT(TIM_HandleTypeDef *htim);
/* Non-Blocking mode: DMA */
HAL_StatusTypeDef HAL_TIM_Base_Start_DMA(TIM_HandleTypeDef *htim, uint32_t *pData, uint16_t Length);
HAL_StatusTypeDef HAL_TIM_Base_Stop_DMA(TIM_HandleTypeDef *htim);
HAL_TIM_PeriodElapsedCallback();//中断回调函数
8.3.2 PWM
cpp
/* Timer PWM functions *********************************************************/
HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);
HAL_StatusTypeDef HAL_TIM_PWM_DeInit(TIM_HandleTypeDef *htim);
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim);
void HAL_TIM_PWM_MspDeInit(TIM_HandleTypeDef *htim);
/* Blocking mode: Polling */
HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
HAL_StatusTypeDef HAL_TIM_PWM_Stop(TIM_HandleTypeDef *htim, uint32_t Channel);
/* Non-Blocking mode: Interrupt */
HAL_StatusTypeDef HAL_TIM_PWM_Start_IT(TIM_HandleTypeDef *htim, uint32_t Channel);
HAL_StatusTypeDef HAL_TIM_PWM_Stop_IT(TIM_HandleTypeDef *htim, uint32_t Channel);
/* Non-Blocking mode: DMA */
HAL_StatusTypeDef HAL_TIM_PWM_Start_DMA(TIM_HandleTypeDef *htim, uint32_t Channel, uint32_t *pData, uint16_t Length);
HAL_StatusTypeDef HAL_TIM_PWM_Stop_DMA(TIM_HandleTypeDef *htim, uint32_t Channel);
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1);
void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2);
void TIM_SetCompare3(TIM_TypeDef* TIMx, uint16_t Compare3);
void TIM_SetCompare4(TIM_TypeDef* TIMx, uint16_t Compare4);
8.4 实验6:定时器输入捕获实验
第一步
第二步:配置
第三步:生成代码
第四步:写程序
定义变量
cpp
uint8_t CaptureSta=0;//输入捕获状态
uint16_t CaptureVal=0;//输入捕获值
定时器输入捕获中断处理回调函数
cpp
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM2)//确定中断的是TIM2
{
if((CaptureSta&0x80)==0)//还没有成功捕获
{
if(CaptureSta&0x40)//捕获到一个下降沿
{
CaptureSta|=0x80;//标记成功捕获到一次高电平脉冲
CaptureVal=HAL_TIM_ReadCapturedValue(&htim2,TIM_CHANNEL_1);//获得捕获值
TIM_RESET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_1);//一定要重新设置
TIM_SET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_1,TIM_ICPOLARITY_RISING);//
}
else
{
CaptureSta=0;
CaptureVal=0;
CaptureSta|=0x40;
__HAL_TIM_DISABLE(&htim2);
__HAL_TIM_SET_COUNTER(&htim2,TIM_CHANNEL_1);
TIM_RESET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_1);
TIM_SET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_1,TIM_ICPOLARITY_FALLING);
__HAL_TIM_ENABLE(&htim2);
}
}
}
}
定时器溢出中断回调函数
cpp
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM2)
{
if((CaptureSta&0x80)==0)
{
if(CaptureSta&0x40)
{
if((CaptureSta&0x3F)==0x3F)
{
TIM_RESET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_1);
TIM_SET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_1,TIM_ICPOLARITY_FALLING);
CaptureSta|=0X80;
CaptureVal=0xFFFF;
}
else
{
CaptureSta++;
}
}
}
}
}
printf重定向
cpp
int fputc(int c,FILE *stream)//printf
{
uint8_t ch[1]={c};
HAL_UART_Transmit(&huart1,ch,1,0xFFFF);
return c;
}
main函数
cpp
while (1)
{
/* USER CODE END WHILE */
if(CaptureSta&0x80)//成功捕获一次高电平
{
int temp=CaptureSta&0x3F;
temp*=1000000;//溢出时间总和
temp+=CaptureVal;//得到总的高电平时间
printf("HIGH:%d us\r\n",temp);
CaptureSta=0;//开启下一次捕获
}
int t;
t++;
if(t>20)//检验程序正常进行
{
t=0;
HAL_GPIO_TogglePin(GPIOF,GPIO_PIN_9);
}
HAL_Delay(10);
/* USER CODE BEGIN 3 */
/* USER CODE END 3 */
}
第五步:编译+写入
8.5 实验7:舵机驱动
我们打开TIM3定时器,这个定时器是挂在APB1总线上的,根据配置好的时钟树可知
频率是84MHz
然后为了方便计算,我们把分频系数设置成84-1,这样相当于1秒钟点1000000次
由于舵机的驱动信号是50Hz,周期是0.02s,所以在一个周期内会点20000次
舵机的转动角度是按照高电平的占空比来计算的
经过手算,我们可以大致推出角度和比较值之间的关系,舵机的难点也就在这了
舵机 单片机
信号线(黄)-------PA6(根据自己的定时器而定)
电源线(红)-------ST-LINK上的5V(9/10)
地线(棕)----------GND
9 I2C
9.1 简介
10 SPI
10.1 简介
..