【1】STM32简介
-
命名规范
STM32F051K8U6
ST - 公司
M - micro eletronics
32 - 32位处理器 STM8
F - 基础版 L - 低功耗 G - 电源
051 - 入门级别 103 - 主流级别 407 - 高性能
K - 32个管脚
8 - 64K
U - UQFN 封装类型
6 - 工作温度范围 -
STM32
和ARM
的关系?
STM32
的CPU
是采用的ARM-CortexM0
架构的
ARM
处理器命名规范
ARM7\9\11
Cortex - A : 开放式操作系统 手机 智能电视
Cortex - R : 实时系统 汽车电子
Cortex - M : 低成本的优化解决方案
实时操作系统:一般用于单片机
分时操作系统:通过时间片轮转进行调度工作 Linux典型的分时操作系统
最大区别就是响应速度。
处理器和架构?
三星S5P6818 ARM-cortexA53 v8
麒麟990 4ARM-cortexA55+4ARM-cortexA76 v8
【2】Cortex-M0架构
Cortex-M0
主要功耗和性能的平衡
Cortex-M0
微处理器主要包括处理器内核、嵌套向量中断控制器(NVIC
)、调试子系统、内部总线系统构成,通过高性能总线(AHB-LITE
)与外部进行通信。
两种工作模式:
- 线程模式(Thread Mode):芯片复位后,即进入线程模式,执行用户程序;
- 处理模式(Handler Mode):当处理器发生了异常或者中断,则进入处理模式进行处理、处理完成后返回线程模式
两种工作状态:
Thumb
状态:正常运行时处理器的状态- 调试状态:调试程序时处理器的状态
相关寄存器:
R0-R12
13个通用寄存器R13
(SP
栈指针):Cortex-M0
在不同物理位置上存在两个栈指针,主栈指针MSP
,进程栈指针PSP
。在处理模式下,只能使用主堆栈,在线程模式下,可以使用主堆栈也可以使用进程堆栈,这主要是由CONTROL
寄存器控制完成。系统上电的默认栈指针是MSP
。R14
(LR
链接寄存器):程序跳转时保存当前正在执行的下一条指令地址。R15
(PC
程序计数器):存储下一条将要执行的指令的地址。- 特殊寄存器:
xPSR
:组合程序状态寄存器,该寄存器由三个程序状态寄存器组成- 应用
PSR
(APSR
):包含前一条指令执行后的条件标志 - 中断
PSR
(IPSR
):包含当前ISR
的异常编号 - 执行
PSR
(EPSR
):包含Thumb
状态位
CortexM0支持的异常和中断:
Cortex-M0
处理器最多支持 32 个外部中断(通常称为 IRQ)和一个不可屏蔽中断(NMI),
另外 Cortex-M0
还支持许多系统异常(Reset、HardFault、SVCall、PendSV、SysTick
)
指令集:
-
ARM
指令集
32位精简指令集;
指令长度固定;
降低编码数量产生的耗费,减轻解码和流水线的负担; -
Thumb
指令集
Thumb
指令集是ARM
指令集的一个子集;
指令宽度16位;
与32位指令集相比,大大节省了系统的存储空间;
Thumb
指令集不完整,所以必须配合ARM
指令集一同使用。
【3】CortexM0的寄存器映射
- 寻址空间 0-4G
32位 2^32次方
4G = 4 * 1024 * 1024 * 1024 = 4,294,967,296 - 寄存器地址映射
在编程手册中找到GPIO相关寄存器,GPIOA的起始地址为0x28000000
MODER
0x00
OTYPER
0x04
#define GPIOA_BASE ((usigned int)0x28000000)
#define GPIOA_OTYPER *(unisgned int *)(GPIOA_BASE+0x04)
封装成结构体:
typedef struct
{
__IO uint32_t MODER; /*!< GPIO port mode register, Address offset: 0x00 */
__IO uint32_t OTYPER; /*!< GPIO port output type register, Address offset: 0x04 */
__IO uint32_t OSPEEDR; /*!< GPIO port output speed register, Address offset: 0x08 */
__IO uint32_t PUPDR; /*!< GPIO port pull-up/pull-down register, Address offset: 0x0C */
__IO uint32_t IDR; /*!< GPIO port input data register, Address offset: 0x10 */
__IO uint32_t ODR; /*!< GPIO port output data register, Address offset: 0x14 */
__IO uint32_t BSRR; /*!< GPIO port bit set/reset register, Address offset: 0x1A */
__IO uint32_t LCKR; /*!< GPIO port configuration lock register, Address offset: 0x1C */
__IO uint32_t AFR[2]; /*!< GPIO alternate function low register, Address offset: 0x20-0x24 */
__IO uint32_t BRR; /*!< GPIO bit reset register, Address offset: 0x28 */
} GPIO_TypeDef;
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
GPIOA->OTYPER = 0X20;
位操作赋值:
- 清零用与,置一用或。
将GPIOA->OTYPER
的第6位置1,第7位置0。
先清零后置位:
GPIOA->OTYPER = GPIOA->OTYPER & ~(0x3 << 6) | (1<<6)
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOA_BASE (AHB2PERIPH_BASE + 0x00000000UL)
#define AHB2PERIPH_BASE (PERIPH_BASE + 0x08000000UL)
#define PERIPH_BASE 0x40000000UL /*!< Peripheral base address in the alias region */
- 实际GPIOA的基地址为0x40000000+ 0x08000000 = 0x48000000
【4】启动文件
Stack_Size EQU 0x400 //初始化栈空间为1K
AREA STACK, NOINIT, READWRITE, ALIGN=3 //定义一个段STACK,未初始化,可读可写,ALIGN=3 以2^3 = 8字节对齐
Stack_Mem SPACE Stack_Size
__initial_sp
Heap_Size EQU 0x200 //初始化堆空间为512字节
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
【实验】通过寄存器点亮LED灯
- 点亮
D10
绿灯 -LED4
- 核心板PB0
让PB0
这个管脚输出低电平即可
- 使能
GPIOB
的时钟
将RCCAHBENR
的第18位置1
RCC->AHBER |= 1<<18;
- 配置
PB0
为输出模式
将GPIOBMODER
的第0位置1,第1位清零。
GPIOB->MODER &= ~(0x3);
GPIOB->MODER |= 0x1;
- 配置
PB0
为推挽输出
将GPIOBOTYPER
的第0位清零;
GPIOB-> OTYPER &= ~0x1
- 将
GPIOBODR
的第0位清零
GPIOB->ODR &= ~0x1
【5】GPIO 通用输入输出口
1.输出类型
三极管(流控)
放大状态:发射结(BE)正偏,集电结(BC)反偏。
饱和状态:发射结正偏,集电结正偏。
截止状态:发射结反偏,集电结反偏。
推挽输出:具备输出高低电平的能力。
开漏输出:具备输出低电平的能力,可通过外加上拉电阻输出高电平。
2.输入类型
浮空输入 :IO -> 施密特触发器 -> 输入寄存器 -> 读
模拟输入 : IO -> 输入寄存器 -> 读
上拉输入 : IO -> 上拉电阻 -> 施密特触发器 -> 输入寄存器 -> 读
下拉输入 :IO -> 下拉电阻 -> 施密特触发器 -> 输入寄存器 -> 读
3.GPIO相关寄存器
- 4个32位配置寄存器
GPIOx_MODER
模式寄存器GPIOx_OTYPER
输出类型寄存器GPIOx_OSPEEDR
输出速度寄存器GPIOx_PUPDR
上拉下拉寄存器
浮空取决于外设的电平
上拉 - 高
下拉 - 低
- 2个32位数据寄存器
GPIOx_IDR
输入数据寄存器GPIOx_ODR
输出数据寄存器
- 1个32位 置位/复位寄存器
GPIOx_BSRR
- 2个32位 复用功能配置寄存器
GPIOx_AFRH
复用功能高位寄存器GPIOx_AFRL
复用功能低位寄存器
【实验】点亮一个LED灯
- 查看电路原理图
在底板原理图上找到对应的LED灯 LED2 LED3 LED4
在核心板原理图上找到对应控制管脚 PB2 PB1 PB0 - 查看芯片手册找到对应的控制寄存器
-
开启GPIOB的时钟
RCC_AHBENR
位 18 IOPBEN: GPIOB 时钟使能
由软件置 1 或清 0.
0: GPIOB 时钟关闭
1: GPIOB 时钟开启
RCC->AHBENR |= 1<<18 -
选择输出模式
GPIOB_MODER 010101
MODERy[1:0]: 端口 x 配置位 (y = 0…15)
这些位可由软件写来配置 I/O 口模式。
00: 输入模式 ( 复位状态 )
01: 通用输出模式
10: 复用功能模式
11: 模拟模式
GPIOB->MODER |= (1<<0)|(1<<2)|(1<<4) -
选择输出类型
GPIOx_OTYPER
OTy[1:0]: 端口 x 的配置位 (y = 0…15)
这些位可由软件写来配置 I/O 口的输出类型。
0: 推挽输出 ( 复位状态 )
1: 开漏输出
GPIOB->OTYPER = 0x0; -
配置输出数据
GPIOB_BSRR 端口置位 / 复位寄存器
位 31:16 BRy: 端口 x 复位位 y(y = 0…15)
这些位只写。读这些位时返回 0x0000 数值。
0: 对相应的 ODRx 位无影响
1: 复位相应的 ODRx 位
GPIOB -> BSRR = (0x7 << 16);
-
//LED灯初始化
void LED_Init()
{
RCC->AHBENR |= 1<<18; //使能GPIOB的时钟
GPIOB->MODER |= (1<<0)|(1<<2)|(1<<4); //配置PB0为输出模式
GPIOB->OTYPER = 0x0; //配置为推挽输出
}
void main()
{
while (1)
{
//实现三个LED灯闪烁
GPIOB -> BSRR = (0x7 << 16);//复位PB0 PB1 PB2输出低电平
HAL_Delay(500);
GPIOB -> BSRR = 0x7; //置位PB0 PB1 PB2 输出高电平
HAL_Delay(500);
}
}
【6】HAL库编程版本
- GPIO写函数
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
功能:给定GPIO引脚写入指定数据
参数:
GPIO_TypeDef* GPIOx 端口 A...F
uint16_t GPIO_Pin 引脚编号 0 - 15
GPIO_PinState PinState
GPIO_PIN_RESET: 清0 低电平
GPIO_PIN_SET: 置1 高电平
返回值:空
void MX_GPIO_Init(void) //LED初始化函数
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE(); //使能GPIOB的时钟
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2, GPIO_PIN_RESET);
//配置PB0 PB1 PB2 初始状态为输出低电平
GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; //配置输出模式为推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; //无上拉下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; //输出速度为低速
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); //使能GPIOB组
}
- HAL库实现流水灯:
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0|GPIO_PIN_2,GPIO_PIN_SET);
HAL_Delay(200);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_2,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0|GPIO_PIN_1,GPIO_PIN_SET);
HAL_Delay(200);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1|GPIO_PIN_2,GPIO_PIN_SET);
HAL_Delay(200);
【实验】 五向按键控制实验
- 五向按键通过一个或门由
D3&KEY
输出
只要有一个方向按键按下,则能够从D3&KEY
这个管脚读到高电平 - 核心板
PA8
管脚连接D3&KEY
读PA8
管脚输入的信号即可判断按键是否被按下 - 读
PA8
管脚的值
HAL_GPIO_ReadPin
GPIO_PinState HAL_GPIO_ReadPin (GPIO_TypeDef * GPIOx,uint16_t GPIO_Pin)
功能:读管脚的电平值
参数:GPIOx
端口号
GPIO_Pin
管脚编号
返回值:GPIO_PinState
管脚实际的电平值
if(HAL_GPIO_ReadPin(KEY_GPIO_Port,KEY_Pin) == 1)
{
HAL_GPIO_WritePin(LED4_GPIO_Port,LED4_Pin,0);
}else
{
HAL_GPIO_WritePin(LED4_GPIO_Port,LED4_Pin,1);
}
HAL_Delay(100);
void HAL_GPIO_TogglePin (GPIO_TypeDef * GPIOx, uint16_tGPIO_Pin)
功能:电平翻转
参数:GPIOx
端口号
GPIO_Pin
管脚编号
返回值:空
//按键按一次 LED切换一次状态
if(HAL_GPIO_ReadPin(KEY_GPIO_Port,KEY_Pin) == 1)
{
while(HAL_GPIO_ReadPin(KEY_GPIO_Port,KEY_Pin) == 1);
//抬手监测
HAL_GPIO_TogglePin(LED4_GPIO_Port,LED4_Pin);
}
【7】通信相关基础知识
- 通信对象最少要有两个
-
根据时钟源可以区分为:
- 同步通信:通信双方共用一个时钟信号。
- 异步通信:通信双方各自都有一个独立时钟源,双方约定好通信速度。
-
通过通信方式区分为:
- 串行通信:用一根数据线进行通信,同一时刻只接收一个bit位数据
优点:节省资源,占用管脚少
缺点:通信效率低 - 并行通信:用多根信号线同时进行同时,同一时刻能够接收多个bit位数据
优点:通信效率高
缺点:占用管脚资源多
- 串行通信:用一根数据线进行通信,同一时刻只接收一个bit位数据
-
根据传输方向区分为:
单工:只能作为接收设备或者发送设备,要么收要么发。 广播、收音机
半双工:可以接收也可以发送,但是同一时间只能发送或接收。对讲机
全双工:同一时间既可作为发送端又可作为接收端。手机
-
【8】串口USART – 通用同步异步收发器
UART
– 异步通信 串行 全双工
USART
– 同步通信
(5v)TTL
电平 : 逻辑1 :2.4v ~ 5v
逻辑0: 0 ~ 0.5v
EIA电平(RS232): 逻辑1 : -3v ~ -15v 逻辑0:+3v ~ +15v 15m
RS485 :双绞线 差分信号 1300m
逻辑1 : 2v ~ 6v 逻辑0:-2v ~ -6v
单片机 — 串口线 — 电脑
CH340
USB转串口芯片
【9】串口通信协议
起始位(1位):低电平
数据位(8位):
校验位(1位):奇偶校验
停止位(1位):高电平
奇校验:数据位中1的个数+校验位上1的个数为奇数
偶校验:数据位中1的个数+校验位上1的个数为偶数
例如:
发送01010101采用奇校验,校验位应该为1
数据接收过程 : RX -> 接收移位寄存器 -> 接收数据寄存器 -> CPU or DMA
数据发送过程 : CPU or DMA -> 发送数据寄存器 -> 发送移位寄存器 —> TX
【10】串口相关寄存器
配置寄存器:
控制寄存器 USART_CR1 CR2
波特率寄存器 USART_BRR
发送数据寄存器:
中断和状态寄存器USART_ISR
接收数据寄存器 USART_RDR
发送数据寄存器USART_TDR
【11】串口发送实验
- 分析原理图
PA9
–USART1_TX
PA10
–USART1_RX
- 配置
CubeMX
使能USART1
- 异步通信Asynchronous
配置USART1
参数
波特率 –115200
数据位 –8位
校验位 –NONE
停止位 –1位
- 编写发送函数
void My_Putchar(uint8_t ch)
{
while(!(USART1->ISR & (1<<7)));
USART1->TDR = ch;
}
【12】串口接收实验
完成串口接收函数
实现大小写转换
例如:通过串口给单片机发送大写字母A,单片机返回小写字母a。
先实现串口接收函数:
uint8_t My_Getchar(void)
{
uint8_t ch;
while(!(USART1->ISR & (1<<5))); //等待接收寄存器非空
ch = USART1->RDR;
return ch;
}
在main函数里实现:
while(1){
ch = My_Getchar();
if(ch>='A'&&ch<='Z')
{
My_Putchar(ch + 32);
}else if(ch>='a'&&ch<='z')
{
My_Putchar(ch - 32);
}else
{
My_Putchar('?');
}
}
【13】HAL库函数版本
- HAL库版本发送数据:
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
UART_HandleTypeDef *huart
: 句柄 (注意取地址)
uint8_t *pData
: 发送的字符数组首地址
uint16_t Size
: 发送数据长度
uint32_t Timeout
: 超时时间(规定时间内没完成发送返回HAL_TIMEOUT
HAL_UART_Transmit(&huart1, "hello test\n", strlen("hello test\n"), 100);
-
HAL库版本接收数据:
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
-
通过HAL库函数实现串口收发:
uint8_t str[32] = {0};
while(1){
HAL_UART_Receive(&huart1, str,sizeof(str), 100);
HAL_UART_Transmit(&huart1, str, strlen(str), 100); //用strlen记得添加string.h文件
memset(str,0,sizeof(str));
}
【13】printf重定向到串口工具实验
a想要发送一个不定长度的到串口
将printf打印的内容输出到串口中,因为printf使用标准库fputc
那么我们将fputc
重写即可
添加stdio.h
头文件,编译后查找fputc
函数 将函数原型找出来
extern _ARMABI int fputc(int /*c*/, FILE * /*stream*/) __attribute__((__nonnull__(2)));
重写fputc函数:
int fputc(int ch, FILE *file)
{
while(!((huart1.Instance->ISR) & (1<<7)));
huart1.Instance->TDR = ch;
return ch;
}
007A1200
02DC6C00
在main.c里测试printf:
printf(“hello world\n”);
【13】scanf输入重定向实验
重写fgetc
函数
extern _ARMABI int fgetc(FILE * /*stream*/) __attribute__((__nonnull__(1)));
int fgetc(FILE * file)
{
int ch;
while(!((huart1.Instance->ISR) & (1<<5)));
ch=huart1.Instance->RDR;
return ch;
}
在main
函数里通过scanf和printf实现输入输出:
scanf("%s",buf);
printf("%s",buf);
注意: 在串口工具里发送字符串时记得添加/r/n结束标志 (可参考群里图片设置自动发送附加位)
【14】中断介绍
-
概念:当处理器在正常执行程序时,突然有外部/内部中断事件发生,
此时需要暂停当前正在执行的程序,并进行备份,转而去处理中断事件,
处理完成后,返回当时程序执行位置继续执行。 -
举例
老师正在讲课 (正常执行的程序)
老徐叫我去开会 (外部中断信号)
暂停讲课保存课堂笔记 (压栈保护现场)
跳转到中断处理程序 (根据异常向量表进行跳转)
开会 (执行中断处理程序)
会后回来继续上课 (出栈恢复现场) -
意义:提高突发事件的处理效率
-
NVIC 嵌套向量中断控制器
-
管理中断
每一个外部中断进来都可能被执行或禁止,并可以设置为挂起状态或清除状态。
挂起:当处理器正在处理高优先级事件时,低优先级中断事件会先被设置为挂起状态。
清除:当处理完中断处理程序时,可将该中断事件设置为清除状态。 -
支持中断或异常的向量化处理
当异常或中断发生时,给定PC一个特定的地址,对应各个中断或异常处理程序的执行地址,这个按照优先级排列组成的地址列表称为中断向量表。
异常:来自于处理器内部的异常事件
中断:来自于处理器外部的中断事件 -
支持中断嵌套
- 有3个固定优先级,都是负值,不可改变。
- 优先级数越小,优先级越高。
- 由两个bit位控制的可配置优先级 00 01 10 11
- 抢占式优先级
抢占,高优先级中断事件可以打断正在处理的低优先级中断事件
响应式优先级
两个中断事件同时到达时,处理响应优先级高的事件。
-
【实验】按键中断
按键按下,触发中断控制串口打印“interrupt”
-
查看芯片原理图
按键 – PA8
使能UART1为异步通信模式 – PA9 PA10 -
在CubeMX中进行配置
- uart1配置为ASY异步模式
- PA8管脚配置为GPIO_EXTI8外部中断模式
- 在NVIC详细配置里使能外部中断EXTI4to15
-
打开工程
启动文件中找到对应的中断向量
DCD EXTI0_1_IRQHandler ; EXTI Line 0 and 1
DCD EXTI2_3_IRQHandler ; EXTI Line 2 and 3
DCD EXTI4_15_IRQHandler ; EXTI Line 4 to 15
-
找到中断处理函数
void EXTI4_15_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_8);
}
- 继续跳转
外部中断处理程序
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin); //清楚中断标志
HAL_GPIO_EXTI_Callback(GPIO_Pin); //处理中断回调函数
}
}
- 进入中断回调函数
__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) //__weak弱化符 可重写该函数
{
UNUSED(GPIO_Pin);
}
在gpio.c里重写中断回调函数:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_8) //判断是否为EXTI8中断事件
{
printf("interrupt!"); //打印“interrupt”
}
}
【15】串口中断
-
串口发送完成中断 串口发送“hello”完成之后触发中断事件,在中断中发送“world”。
- 在CubeMX中将UART1设置为异步模式
- 在NVIC中使能串口全局中断
USART1 global interrupt I USART1 wake-up interrupt through EXTI line 25 - 生成工程后编译,在启动文件中找到对应中断向量:
DCD USART1_IRQHandler ; USART1
void USART1_IRQHandler(void) { HAL_UART_IRQHandler(&huart1); }
- 找到串口中断处理函数
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
发送完成中断
UART_EndTransmit_IT(huart);
- 发送完成中断回调函数
static void UART_EndTransmit_IT(UART_HandleTypeDef *huart)
{
ATOMIC_CLEAR_BIT(huart->Instance->CR1, USART_CR1_TCIE);
huart->gState = HAL_UART_STATE_READY;
huart->TxISR = NULL;
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
huart->TxCpltCallback(huart);
#else
HAL_UART_TxCpltCallback(huart);
#endif
}
- 重新发送完成中断回调函数
__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
UNUSED(huart);
}
在uart.c中重写
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_UART_Transmit(&huart1,"world",sizeof("world"),100);
}
- 在main.c中触发发送完成中断
HAL_UART_Transmit_IT(&huart1,"hello",sizeof("hello"));
-
串口接收完成中断
//重写接收完成中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_UART_Transmit(&huart1,"Recive 4 char",sizeof("Recive 4 char"),100);
}
//main.c中触发接收中断
while (1)
{
HAL_UART_Receive_IT(&huart1,str,4); //接收4个字符 触发一次中断
HAL_Delay(1000);
printf("%s",str);
memset(str,0,sizeof(str));
}
【实验】五向按键电压采集实验
- 查看电路原理图
找到按键ADC监测管脚 – PA0 - 配置CubeMX
配置PA0管脚为ADC_CH0
使能串口为异步通信模式
使能RCC时钟配置时钟选择高速外部时钟48Mhz - 生成工程
需要使用的函数:
HAL_ADC_Start 开始转换
HAL_ADC_PollForConversion 等待转换
HAL_ADC_GetValue 获取转换结果
HAL_ADC_Stop 停止转换 - 代码实现:
while(1)
{
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_8))
{
HAL_Delay(100);
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_8))
{
HAL_ADC_Start(&hadc);
HAL_ADC_PollForConversion(&hadc,100);
value = HAL_ADC_GetValue(&hadc);
HAL_ADC_Stop(&hadc);
printf("key:%d",value);
}
}
【16】STM32的时钟系统
- 时钟系统是由振荡器(信号源),定时唤醒器,分频器等组成的电路。
常用的信号源:石英晶体 RC振荡器 - 震荡不稳 停振 不起振
- 倍频:CPU需要更高的频率,石英晶体成本高,震荡频率高不稳定,所以需要把低频率倍频到高频率
分频: CPU频率很高但是外设不需要这么高的频率 - STM32四个时钟源
- HSI 高速内部时钟 RC振荡器 8MHZ
- HSE 高速外部时钟 石英晶体/陶瓷 8MHZ
- LSI 低速内部时钟 RC振荡器 40KHZ 看门狗用的时钟源只能是这个,有时候也给RTC时钟源
- LSE 低速外部时钟 32.768KHZ的使用晶体 给RTC做时钟源
- PLL(倍频器) 图10-时钟树
【17】systick定时器
- 定时器: 能够计数和定时的器件称为定时器,systick又叫滴答定时器,是一个定时设备,(定时的本质是计数-保证时钟信号是周期性的),systick位于Contex_M0的内核当中,和NVIC是捆绑的,产生一个systick的异常,IRQ异常号是15
- 滴答定时器 是一个24位的递减定时器,最多能计数2^24(0xFFFFFF),减到0的时候出发中断,再次重装值继续减1
【实验】 追Systick - 8M的时基
- 配置CubeMx 时钟树-高速内部时钟-8M(使用默认)
- DCD SysTick_Handler
void SysTick_Handler(void)
{
HAL_IncTick();
}
__weak void HAL_IncTick(void)
{
uwTick += uwTickFreq;
//uwTick +=1 ; --> uwTick++;
}
__IO uint32_t uwTick; // __IO防止编译器优化 uwTick是个全局变量 初始值是0
HAL_TickFreqTypeDef uwTickFreq = HAL_TICK_FREQ_DEFAULT; //枚举类型
HAL_TICK_FREQ_1KHZ = 1U //就是1
HAL_TICK_FREQ_DEFAULT = HAL_TICK_FREQ_1KHZ
- main.c
/* Reset of all peripherals, Initializes the Flash interface and the Systick. /
初始化外设 flash和Systick
HAL_Init();
/ Use systick as time base source and configure 1ms tick (default clock after Reset is HSI) */
使用高速内部时钟配置systick时基是1ms
(就算配置了48M在复位之后第一次也是HSI-8M)
HAL_InitTick(TICK_INT_PRIORITY);
Care must be taken:
外设中断调用HAL_Dela的时候,要注意中断优先级的问题,必须要提高systick的优先级(数字越小越高),否则会遇到阻塞/屏蔽的情况
- 1.中断处理函数为什么要延时? 2.NVIC捆绑内核组件,两个中断优先级会有影响。
/Configure the SysTick to have interrupt in 1ms time basis/
if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U)
{
return HAL_ERROR;
}
uint32_t SystemCoreClock = 8000000;
HSI - 8MHZ 1/8M秒可以计一个数,计了8M/1000个数
8M/(1000/1) == 1/8M8M/1000=1/1000s = 1ms
uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb)
{
return SysTick_Config(TicksNumb);
}
SysTick->LOAD = (uint32_t)(ticks - 1UL); / set reload register 配置重装寄存器*/
NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); //设置优先级
【实验】 HAL_Delay函数
__weak void HAL_Delay(uint32_t Delay) //想要延时的毫秒数
{
uint32_t tickstart = HAL_GetTick();
//tickstart一上来是1
uint32_t wait = Delay; //想要延时的值放到wait里
if (wait < HAL_MAX_DELAY) //判断延时时间是否合法
{
//wait += (uint32_t)(uwTickFreq);
//wait +=1; wait ++;
}
while((HAL_GetTick() - tickstart) <= 10*wait)
{
1-1 < 11
2-1 < 11 1-11 --- 10ms
12 - 1= 11
}
}
__weak uint32_t HAL_GetTick(void)
{
return uwTick; //返回的是不断自增的uwTick值
}
【实验】 48M高速外部时钟 - 查看重装值
上电之后还是8M 一次过后会改变成我们设置的值
- debug
SysTick->LOAD = (uint32_t)(ticks - 1UL); - 搜索
/* Update the SystemCoreClock global variable */
SystemCoreClock = HAL_RCC_GetSysClockFreq() >> AHBPrescTable[(RCC->CFGR & RCC_CFGR_HPRE)>> RCC_CFGR_HPRE_BITNUMBER];
/* Configure the source of time base considering new system clocks settings*/
HAL_InitTick (TICK_INT_PRIORITY);
【18】通用定时器
- F051有6个通用定时器,一个基本定时器,一个高级定时器
- 通用定时器: TIM2 3 14 15 16 17
定时器的定时和计数
输入捕获
输出比较 – 输出PWM波形 - 基本定时器 TIM6
定时器的定时和计数 - 高级定时器
带死区的PWM和刹车功能
- 通用定时器: TIM2 3 14 15 16 17
- 定时器计数模式
向上计数 计数器从0开始向上加到加载值(TIMx_ARR)触发一个计数器向上计数溢出中断,然后从0开始重新计数
向下计数 计数器从重装值(TIMx_ARR)开始向下减,减到0触发向下计数溢出中断,然后重装初始值
中央对齐(向上向下) 计数器从0开始向上加到加载值(TIMx_ARR),
计数器从重装值(TIMx_ARR)开始向下减
- 假设现在频率是48M 用定时器实现1S的定时
48 / 48(0-47) = 1MHZ * 1M 个 = 1S
频率HZ 周期S 计数个
-
输入捕获和输出比较
1)输入捕获 :用来获取外部事件的
2)输出比较: 定时器从0开始到CCR的值输出低电平,从CCR到ARR的值输出高电平,循环此过程
CCRX:占空比 ARR:周期
PWM:脉冲宽度调制 -
寄存器
TIMx_CNT 计数器当前的值
TIMx_ARR 自动重装值
TIMx_CCRx 捕获/比较寄存器
【实验】 用通用定时器TIM2实现定时1S中断在中断处理函数里打印字符
- CubeMx配置
使用HSI 写48M
USART - 115200
Precasler=48-1 //分频
conter = 1000000-1 //重装值1M个
NVIC里中断打开 - DCD TIM2_IRQHandler
void TIM2_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim2);
}
/* TIM Update event */ 定时器数值更新
if (__HAL_TIM_GET_FLAG(htim, TIM_FLAG_UPDATE) != RESET)
{
if (__HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_UPDATE) != RESET)
{
__HAL_TIM_CLEAR_IT(htim, TIM_IT_UPDATE);
HAL_TIM_PeriodElapsedCallback(htim);
}
}
【实验】 软件定时器 实现上述功能 模拟HAL_Delay函数
-
设计思想:
-
设计一个定时器 (软件层次 结构体:开始时间 想要延时的时间)
-
函数: 设定定时时间的函数 比较定时时间是否达到
-
CubeMX
新建一个工程 48M 开两个LED
-
新建文件
file -new softtimer.c --> core/src
file -new softtimer.h --> core/inc
在Application user上右键选择Add existing file将.c文件添加进来(.h不需要这个操作) -
softtimer.h
里
#ifndef __SOFTTIMER_H__
#define __SOFTTIMER_H__
#include "stm32f0xx.h"
—>是在左边Driver/CMSIS
的文件夹下的system_stm32f0xx.c
下找到的头文件
#endif
-
-
softtimer.c
里
setTimer compareTimer
两个函数 -
main.c
文件 测试功能
【19】ADC模数转化
-
ADC逐次逼近形的模拟-数字转化器(采样 量化 编码)
*模拟信号和数字信号的区别?是否连续
*dht11-温湿度传感器-直接出数字信号 -
ADC特性
量程:能测量的电压的范围
分辨率: ADC的分辨率通常以二进制数表示,位数越多分辨率越高,转化时间越长
转化时间:从转化开始到获得稳定的数字信号的时间
19个通道:16个外部(对应16个引脚),3个内部(芯片的温度 定时器的信息 内部参考电压)
ADC的结果以左对齐或者右对齐的方式存在16位的寄存器当中
ADC最高的频率14M(时钟树查看) -
转化方式
单通道单单次转化
单通道连续转化
多通道扫描单次转化
多通道扫描连续转化
间隔转化
存在AD的中断和状态寄存器ADC_ISR
EOC:通道转化结束
EOS:序列转化结束
【实验】 基于五项按键的AD采集实验(判断方向,打印上下左右)
-
查看电路图
ADC_KEY - PA0 (GPIO引脚复用映射) - AD_IN0
D3&KEY - PA8 - input
clock pre 时钟分频
resolution 精度
Data Ali 对齐方式 - 左右
scan 扫描方式 - 前后
continue 连续转化
discontinue 间隔
DMA 是否以DMA方式开启AD
End of单通道转化结束的标志
Sampling time 采样时间 -
编程步骤
- 开启ADC HAL_ADC_Start
- 等待ADC转化 HAL_ADC_PollForConversion
- 获取ADC的值 HAL_ADC_GetValue
- 关闭ADC HAL_ADC_Stop
【实验】单通道数据检测 - 光照/火焰传感器 ADC转化完成的中断里得到数据
- 光敏电阻:随着光强的增大电阻减小
- A1 - PA4 — AD_IN4
RCC - 48M
NVIC - AD中断 - DCD ADC1_COMP_IRQHandler
void ADC1_COMP_IRQHandler(void)
{
HAL_ADC_IRQHandler(&hadc);
}
/* Note: into callback, to determine if conversion has been triggered */
/* from EOC or EOS, possibility to use: */
/* " if( __HAL_ADC_GET_FLAG(&hadc, ADC_FLAG_EOS)) " */
#if (USE_HAL_ADC_REGISTER_CALLBACKS == 1)
hadc->ConvCpltCallback(hadc);
#else
HAL_ADC_ConvCpltCallback(hadc);
#endif /* USE_HAL_ADC_REGISTER_CALLBACKS */
中断回调函数
__weak void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
}
【实验】多通道数据检测 - 按键按下中断里检测按键的数值和光强的数值
- PA0 - ADC_IN0 检测按键的数值
PA4 - ADC_IN0 检测传感器的数值
PA8 - EXTI8 开启按键中断NVIC
USART - Asy - 115200
RCC - 48M(如果是8M可能有问题) - 重写按键中断回调函数
【20】DMA 直接存储器访问
- DMA无需CPU的参与,直接控制传输数据,提高CPU的效率
- 传输宽度:字节 半字 全字
向DMA请求优先级:低 中等 高 很高
目标和源传输的数据宽度对齐
每个通道都有三个事件标志位:DMA半传输 传输完成 传输出错
存储器->外设 外设->存储器 存储器->存储器 外设->外设
闪存(flash) SRAM APB AHB都可以做目标或者是源
搬移的最大长度是65535字节 - DMA寄存器
DMA_CPARx 外设寄存器的地址
DMA_CMARx 存储器地址设置
DMA_CCRx 配置数据的传输方向 - DMA增量设置 一般会把存储器设为增量模式
循环模式
【实验】DMA-ADC的多路采集 按键按下触发按键中断 在按键中断里开启ADC请求DMA的方式 采集火焰传感器和按键的数值
- 实验思路
按键按下 触发一个中断 在中断里开启了ADC ADC处理结束的时候自动发起DMA请求 DMA请求控制总线 搬移数据
搬移数据结束之后 产生一个搬移完成的DMA中断 - RCC - 48M
USART - Asy - 115200
PA8 - EXTI8 - NVIC
PA0 - ADC_IN0
PA4 - ADC_IN4
ADC: DMA request 开启DMA请求
DMA setting - ADD - select - ADC
可以看到 配置好使用通道1 方向是外设到内存 优先级低
MODE-normal(正常模式) 如果ADC是连续转化我们要选择circle搬移 不然会溢出
data width选择半字(32位单片机半字是16 ADC-DR也是16位)
Increase address 内存要选择地址增加
NVIC看到DMA默认就开了中断 - DCD DMA1_Channel1_IRQHandler
void DMA1_Channel1_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_adc);
}
/* Transfer Complete Interrupt management*/
if(hdma->XferCpltCallback != NULL)
{
/* Transfer complete callback */
hdma->XferCpltCallback(hdma);
}
void (* XferCpltCallback)(struct __DMA_HandleTypeDef * hdma);
/* Set the DMA transfer complete callback */
hadc->DMA_Handle->XferCpltCallback = ADC_DMAConvCplt;
static void ADC_DMAConvCplt(DMA_HandleTypeDef *hdma)
{
/* Conversion complete callback */
HAL_ADC_ConvCpltCallback(hadc);
}
重写的回调函数
__weak void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
}
【实验】 利用串口空闲中断,使用DMA来搬移串口的数据,实现数据的接收和显示
- 串口空闲的概念 - USART_ISR:第4位 IDLE
串口在接收数据之后,接收一帧空闲帧,线路会被认为空闲状态,如果串口没有数据接收的情况不会产生串口空闲中断,只有在接收第一个数据之后才能触发中断,一旦数据断流没有接收到数据,就会产生串口空闲中断。
IDLE:
0: 没有检测到线路空闲
1: 检测到线路空闲 - 逻辑
- main:
- 开串口接收的DMA(设置通道长度 即每次搬移的数据)
- 开串口空闲中断
- 中断函数:
- 判断是否是串口空闲中断
- 关闭DMA通道 空闲中断清除
- 计算DMA传输的实际长度(通道长度 - 剩余数量 = 实际传输的数量) DMA_CNDTRx:DMA通道传输量寄存器
- 开启串口DMA发送数据 ,将数据发送到串口工具
- 开启DMA接收通道(设置通道长度)
- 需要的函数
HAL_UART_Receive_DMA 开启DMA
__HAL_UART_ENABLE_IT 开启串口空闲中断
__HAL_UART_GET_FLAG 获得串口空闲中断标志
__HAL_UART_CLEAR_FLAG 清除串口中断标志 - 函数编写
- main:
int fputc(int ch,FILE *file)
{
while (!(USART1->ISR & 1<<7));
USART1->TDR = ch;
return ch;
}