STM32基础课程笔记

【1】STM32简介

  1. 命名规范
    STM32F051K8U6
    ST - 公司
    M - micro eletronics
    32 - 32位处理器 STM8
    F - 基础版 L - 低功耗 G - 电源
    051 - 入门级别 103 - 主流级别 407 - 高性能
    K - 32个管脚
    8 - 64K
    U - UQFN 封装类型
    6 - 工作温度范围

  2. STM32ARM的关系?
    STM32CPU是采用的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)与外部进行通信。

两种工作模式:

  1. 线程模式(Thread Mode):芯片复位后,即进入线程模式,执行用户程序;
  2. 处理模式(Handler Mode):当处理器发生了异常或者中断,则进入处理模式进行处理、处理完成后返回线程模式

两种工作状态:

  1. Thumb状态:正常运行时处理器的状态
  2. 调试状态:调试程序时处理器的状态

相关寄存器:

  1. R0-R12 13个通用寄存器
  2. R13SP栈指针):Cortex-M0在不同物理位置上存在两个栈指针,主栈指针 MSP,进程栈指针PSP。在处理模式下,只能使用主堆栈,在线程模式下,可以使用主堆栈也可以使用进程堆栈,这主要是由 CONTROL寄存器控制完成。系统上电的默认栈指针是MSP
  3. R14LR链接寄存器):程序跳转时保存当前正在执行的下一条指令地址。
  4. R15PC程序计数器):存储下一条将要执行的指令的地址。
  5. 特殊寄存器:
    1. xPSR:组合程序状态寄存器,该寄存器由三个程序状态寄存器组成
    2. 应用PSRAPSR):包含前一条指令执行后的条件标志
    3. 中断PSRIPSR):包含当前ISR的异常编号
    4. 执行PSREPSR):包含Thumb状态位

CortexM0支持的异常和中断:

Cortex-M0处理器最多支持 32 个外部中断(通常称为 IRQ)和一个不可屏蔽中断(NMI),
另外 Cortex-M0 还支持许多系统异常(Reset、HardFault、SVCall、PendSV、SysTick

指令集:

  1. ARM指令集
    32位精简指令集;
    指令长度固定;
    降低编码数量产生的耗费,减轻解码和流水线的负担;

  2. Thumb指令集
    Thumb指令集是ARM指令集的一个子集;
    指令宽度16位;
    与32位指令集相比,大大节省了系统的存储空间;
    Thumb指令集不完整,所以必须配合ARM指令集一同使用。

【3】CortexM0的寄存器映射

  1. 寻址空间 0-4G
    32位 2^32次方
    4G = 4 * 1024 * 1024 * 1024 = 4,294,967,296
  2. 寄存器地址映射
    在编程手册中找到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这个管脚输出低电平即可
  1. 使能GPIOB的时钟
    ​ 将RCCAHBENR的第18位置1
    RCC->AHBER |= 1<<18;
  2. 配置PB0为输出模式
    ​ 将GPIOBMODER的第0位置1,第1位清零。
    GPIOB->MODER &= ~(0x3);
    GPIOB->MODER |= 0x1;
  3. 配置PB0为推挽输出
    ​ 将GPIOBOTYPER的第0位清零;
    GPIOB-> OTYPER &= ~0x1
  4. ​ 将GPIOBODR的第0位清零
    GPIOB->ODR &= ~0x1

【5】GPIO 通用输入输出口

1.输出类型

三极管(流控)
放大状态:发射结(BE)正偏,集电结(BC)反偏。
饱和状态:发射结正偏,集电结正偏。
截止状态:发射结反偏,集电结反偏。

推挽输出:具备输出高低电平的能力。
开漏输出:具备输出低电平的能力,可通过外加上拉电阻输出高电平。

2.输入类型

浮空输入 :IO -> 施密特触发器 -> 输入寄存器 -> 读
模拟输入 : IO -> 输入寄存器 -> 读
上拉输入 : IO -> 上拉电阻 -> 施密特触发器 -> 输入寄存器 -> 读
下拉输入 :IO -> 下拉电阻 -> 施密特触发器 -> 输入寄存器 -> 读

3.GPIO相关寄存器

  1. 4个32位配置寄存器
    1. GPIOx_MODER模式寄存器
    2. GPIOx_OTYPER输出类型寄存器
    3. GPIOx_OSPEEDR输出速度寄存器
    4. GPIOx_PUPDR上拉下拉寄存器
      浮空取决于外设的电平
      上拉 - 高
      下拉 - 低
  2. 2个32位数据寄存器
    1. GPIOx_IDR输入数据寄存器
    2. GPIOx_ODR输出数据寄存器
  3. 1个32位 置位/复位寄存器
    GPIOx_BSRR
  4. 2个32位 复用功能配置寄存器
    1. GPIOx_AFRH复用功能高位寄存器
    2. GPIOx_AFRL 复用功能低位寄存器

【实验】点亮一个LED灯

  1. 查看电路原理图
    在底板原理图上找到对应的LED灯 LED2 LED3 LED4
    在核心板原理图上找到对应控制管脚 PB2 PB1 PB0
  2. 查看芯片手册找到对应的控制寄存器
    1. 开启GPIOB的时钟
      RCC_AHBENR
      位 18 IOPBEN: GPIOB 时钟使能
      由软件置 1 或清 0.
      0: GPIOB 时钟关闭
      1: GPIOB 时钟开启
      RCC->AHBENR |= 1<<18

    2. 选择输出模式
      GPIOB_MODER 010101
      MODERy[1:0]: 端口 x 配置位 (y = 0…15)
      这些位可由软件写来配置 I/O 口模式。
      00: 输入模式 ( 复位状态 )
      01: 通用输出模式
      10: 复用功能模式
      11: 模拟模式
      GPIOB->MODER |= (1<<0)|(1<<2)|(1<<4)

    3. 选择输出类型
      GPIOx_OTYPER
      OTy[1:0]: 端口 x 的配置位 (y = 0…15)
      这些位可由软件写来配置 I/O 口的输出类型。
      0: 推挽输出 ( 复位状态 )
      1: 开漏输出
      GPIOB->OTYPER = 0x0;

    4. 配置输出数据
      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库编程版本

  1. 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);

【实验】 五向按键控制实验

  1. 五向按键通过一个或门由 D3&KEY输出
    只要有一个方向按键按下,则能够从D3&KEY这个管脚读到高电平
  2. 核心板PA8管脚连接D3&KEY
    PA8管脚输入的信号即可判断按键是否被按下
  3. 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】通信相关基础知识

  • 通信对象最少要有两个
    1. 根据时钟源可以区分为:

      1. 同步通信:通信双方共用一个时钟信号。
      2. 异步通信:通信双方各自都有一个独立时钟源,双方约定好通信速度。
    2. 通过通信方式区分为:

      1. 串行通信:用一根数据线进行通信,同一时刻只接收一个bit位数据
        优点:节省资源,占用管脚少
        缺点:通信效率低
      2. 并行通信:用多根信号线同时进行同时,同一时刻能够接收多个bit位数据
        优点:通信效率高
        缺点:占用管脚资源多
    3. 根据传输方向区分为:
      单工:只能作为接收设备或者发送设备,要么收要么发。 广播、收音机
      半双工:可以接收也可以发送,但是同一时间只能发送或接收。对讲机
      全双工:同一时间既可作为发送端又可作为接收端。手机

【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】串口发送实验

  1. 分析原理图
    PA9USART1_TX
    PA10USART1_RX
  2. 配置CubeMX
    使能USART1 - 异步通信Asynchronous
    配置USART1参数
    波特率 – 115200
    数据位 –8位
    校验位 – NONE
    停止位 – 1位
  3. 编写发送函数
  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库函数版本

  1. 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);

  1. HAL库版本接收数据:
    HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)

  2. 通过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】中断介绍

  1. 概念:当处理器在正常执行程序时,突然有外部/内部中断事件发生,
    此时需要暂停当前正在执行的程序,并进行备份,转而去处理中断事件,
    处理完成后,返回当时程序执行位置继续执行。

  2. 举例
    老师正在讲课 (正常执行的程序)
    老徐叫我去开会 (外部中断信号)
    暂停讲课保存课堂笔记 (压栈保护现场)
    跳转到中断处理程序 (根据异常向量表进行跳转)
    开会 (执行中断处理程序)
    会后回来继续上课 (出栈恢复现场)

  3. 意义:提高突发事件的处理效率

  4. NVIC 嵌套向量中断控制器

    1. 管理中断
      ​ 每一个外部中断进来都可能被执行或禁止,并可以设置为挂起状态或清除状态。
      ​ 挂起:当处理器正在处理高优先级事件时,低优先级中断事件会先被设置为挂起状态。
      ​ 清除:当处理完中断处理程序时,可将该中断事件设置为清除状态。

    2. 支持中断或异常的向量化处理
      ​ 当异常或中断发生时,给定PC一个特定的地址,对应各个中断或异常处理程序的执行地址,这个按照优先级排列组成的地址列表称为中断向量表。
      异常:来自于处理器内部的异常事件
      ​ 中断:来自于处理器外部的中断事件

    3. 支持中断嵌套

      1. 有3个固定优先级,都是负值,不可改变。
      2. 优先级数越小,优先级越高。
      3. 由两个bit位控制的可配置优先级 00 01 10 11
      4. 抢占式优先级
        ​ 抢占,高优先级中断事件可以打断正在处理的低优先级中断事件
        ​ 响应式优先级
        ​ 两个中断事件同时到达时,处理响应优先级高的事件。

【实验】按键中断

​ 按键按下,触发中断控制串口打印“interrupt”

  1. 查看芯片原理图
    ​ 按键 – PA8
    ​ 使能UART1为异步通信模式 – PA9 PA10

  2. 在CubeMX中进行配置

    1. uart1配置为ASY异步模式
    2. PA8管脚配置为GPIO_EXTI8外部中断模式
    3. 在NVIC详细配置里使能外部中断EXTI4to15
  3. 打开工程
    ​ 启动文件中找到对应的中断向量
    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

  4. 找到中断处理函数

  void EXTI4_15_IRQHandler(void)
	  {
	    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_8);
	  }
  1. 继续跳转
    外部中断处理程序
 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);   //处理中断回调函数
	    }
	  }
  1. 进入中断回调函数
  __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】串口中断

  1. 串口发送完成中断 串口发送“hello”完成之后触发中断事件,在中断中发送“world”。

    1. 在CubeMX中将UART1设置为异步模式
    2. 在NVIC中使能串口全局中断
      USART1 global interrupt I USART1 wake-up interrupt through EXTI line 25
    3. 生成工程后编译,在启动文件中找到对应中断向量:
      DCD USART1_IRQHandler ; USART1
    void USART1_IRQHandler(void)
    {
      HAL_UART_IRQHandler(&huart1);
    }
    
    1. 找到串口中断处理函数
      void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
      发送完成中断
      UART_EndTransmit_IT(huart);
    2. 发送完成中断回调函数
      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
      }
    3. 重新发送完成中断回调函数
      __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);
      }
    4. 在main.c中触发发送完成中断
      HAL_UART_Transmit_IT(&huart1,"hello",sizeof("hello"));
  2. 串口接收完成中断
    //重写接收完成中断回调函数

 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));
  }

【实验】五向按键电压采集实验

  1. 查看电路原理图
    找到按键ADC监测管脚 – PA0
  2. 配置CubeMX
    配置PA0管脚为ADC_CH0
    使能串口为异步通信模式
    使能RCC时钟配置时钟选择高速外部时钟48Mhz
  3. 生成工程
    需要使用的函数:
    HAL_ADC_Start 开始转换
    HAL_ADC_PollForConversion 等待转换
    HAL_ADC_GetValue 获取转换结果
    HAL_ADC_Stop 停止转换
  4. 代码实现:
 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的时钟系统

  1. 时钟系统是由振荡器(信号源),定时唤醒器,分频器等组成的电路。
    常用的信号源:石英晶体 RC振荡器
  2. 震荡不稳 停振 不起振
  3. 倍频:CPU需要更高的频率,石英晶体成本高,震荡频率高不稳定,所以需要把低频率倍频到高频率
    分频: CPU频率很高但是外设不需要这么高的频率
  4. STM32四个时钟源
    1. HSI 高速内部时钟 RC振荡器 8MHZ
    2. HSE 高速外部时钟 石英晶体/陶瓷 8MHZ
    3. LSI 低速内部时钟 RC振荡器 40KHZ 看门狗用的时钟源只能是这个,有时候也给RTC时钟源
    4. LSE 低速外部时钟 32.768KHZ的使用晶体 给RTC做时钟源
  • PLL(倍频器) 图10-时钟树

【17】systick定时器

  1. 定时器: 能够计数和定时的器件称为定时器,systick又叫滴答定时器,是一个定时设备,(定时的本质是计数-保证时钟信号是周期性的),systick位于Contex_M0的内核当中,和NVIC是捆绑的,产生一个systick的异常,IRQ异常号是15
  2. 滴答定时器 是一个24位的递减定时器,最多能计数2^24(0xFFFFFF),减到0的时候出发中断,再次重装值继续减1

【实验】 追Systick - 8M的时基

  1. 配置CubeMx 时钟树-高速内部时钟-8M(使用默认)
  2. 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
  1. 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 一次过后会改变成我们设置的值

  1. debug
    SysTick->LOAD = (uint32_t)(ticks - 1UL);
  2. 搜索
    /* 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】通用定时器

  1. F051有6个通用定时器,一个基本定时器,一个高级定时器
    1. 通用定时器: TIM2 3 14 15 16 17
      定时器的定时和计数
      输入捕获
      输出比较 – 输出PWM波形
    2. 基本定时器 TIM6
      定时器的定时和计数
    3. 高级定时器
      带死区的PWM和刹车功能
  2. 定时器计数模式
    向上计数 计数器从0开始向上加到加载值(TIMx_ARR)触发一个计数器向上计数溢出中断,然后从0开始重新计数
    向下计数 计数器从重装值(TIMx_ARR)开始向下减,减到0触发向下计数溢出中断,然后重装初始值
    中央对齐(向上向下) 计数器从0开始向上加到加载值(TIMx_ARR),
    计数器从重装值(TIMx_ARR)开始向下减
  • 假设现在频率是48M 用定时器实现1S的定时
    48 / 48(0-47) = 1MHZ * 1M 个 = 1S
    频率HZ 周期S 计数个
  1. 输入捕获和输出比较
    1)输入捕获 :用来获取外部事件的
    2)输出比较: 定时器从0开始到CCR的值输出低电平,从CCR到ARR的值输出高电平,循环此过程
    CCRX:占空比 ARR:周期
    PWM:脉冲宽度调制

  2. 寄存器
    TIMx_CNT 计数器当前的值
    TIMx_ARR 自动重装值
    TIMx_CCRx 捕获/比较寄存器

【实验】 用通用定时器TIM2实现定时1S中断在中断处理函数里打印字符

  1. CubeMx配置
    使用HSI 写48M
    USART - 115200
    Precasler=48-1 //分频
    conter = 1000000-1 //重装值1M个
    NVIC里中断打开
  2. 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函数

  1. 设计思想:

    1. 设计一个定时器 (软件层次 结构体:开始时间 想要延时的时间)

    2. 函数: 设定定时时间的函数 比较定时时间是否达到

    3. CubeMX新建一个工程 48M 开两个LED

    4. 新建文件
      file -new softtimer.c --> core/src
      file -new softtimer.h --> core/inc
      在Application user上右键选择Add existing file将.c文件添加进来(.h不需要这个操作)

    5. softtimer.h
      #ifndef __SOFTTIMER_H__
      #define __SOFTTIMER_H__
      #include "stm32f0xx.h" —>是在左边Driver/CMSIS的文件夹下的system_stm32f0xx.c下找到的头文件
      #endif

  2. softtimer.c
    setTimer compareTimer两个函数

  3. main.c文件 测试功能

【19】ADC模数转化

  1. ADC逐次逼近形的模拟-数字转化器(采样 量化 编码)
    *模拟信号和数字信号的区别?是否连续
    *dht11-温湿度传感器-直接出数字信号

  2. ADC特性
    量程:能测量的电压的范围
    分辨率: ADC的分辨率通常以二进制数表示,位数越多分辨率越高,转化时间越长
    转化时间:从转化开始到获得稳定的数字信号的时间
    19个通道:16个外部(对应16个引脚),3个内部(芯片的温度 定时器的信息 内部参考电压)
    ADC的结果以左对齐或者右对齐的方式存在16位的寄存器当中
    ADC最高的频率14M(时钟树查看)

  3. 转化方式
    单通道单单次转化
    单通道连续转化
    多通道扫描单次转化
    多通道扫描连续转化
    间隔转化
    存在AD的中断和状态寄存器ADC_ISR
    EOC:通道转化结束
    EOS:序列转化结束

【实验】 基于五项按键的AD采集实验(判断方向,打印上下左右)

  1. 查看电路图
    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 采样时间

  2. 编程步骤

    1. 开启ADC HAL_ADC_Start
    2. 等待ADC转化 HAL_ADC_PollForConversion
    3. 获取ADC的值 HAL_ADC_GetValue
    4. 关闭ADC HAL_ADC_Stop

【实验】单通道数据检测 - 光照/火焰传感器 ADC转化完成的中断里得到数据

  1. 光敏电阻:随着光强的增大电阻减小
  2. A1 - PA4 — AD_IN4
    RCC - 48M
    NVIC - AD中断
  3. 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)
{

}

【实验】多通道数据检测 - 按键按下中断里检测按键的数值和光强的数值

  1. PA0 - ADC_IN0 检测按键的数值
    PA4 - ADC_IN0 检测传感器的数值
    PA8 - EXTI8 开启按键中断NVIC
    USART - Asy - 115200
    RCC - 48M(如果是8M可能有问题)
  2. 重写按键中断回调函数

【20】DMA 直接存储器访问

  1. DMA无需CPU的参与,直接控制传输数据,提高CPU的效率
  2. 传输宽度:字节 半字 全字
    向DMA请求优先级:低 中等 高 很高
    目标和源传输的数据宽度对齐
    每个通道都有三个事件标志位:DMA半传输 传输完成 传输出错
    存储器->外设 外设->存储器 存储器->存储器 外设->外设
    闪存(flash) SRAM APB AHB都可以做目标或者是源
    搬移的最大长度是65535字节
  3. DMA寄存器
    DMA_CPARx 外设寄存器的地址
    DMA_CMARx 存储器地址设置
    DMA_CCRx 配置数据的传输方向
  4. DMA增量设置 一般会把存储器设为增量模式
    循环模式

【实验】DMA-ADC的多路采集 按键按下触发按键中断 在按键中断里开启ADC请求DMA的方式 采集火焰传感器和按键的数值

  1. 实验思路
    按键按下 触发一个中断 在中断里开启了ADC ADC处理结束的时候自动发起DMA请求 DMA请求控制总线 搬移数据
    搬移数据结束之后 产生一个搬移完成的DMA中断
  2. 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默认就开了中断
  3. 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来搬移串口的数据,实现数据的接收和显示

  1. 串口空闲的概念 - USART_ISR:第4位 IDLE
    串口在接收数据之后,接收一帧空闲帧,线路会被认为空闲状态,如果串口没有数据接收的情况不会产生串口空闲中断,只有在接收第一个数据之后才能触发中断,一旦数据断流没有接收到数据,就会产生串口空闲中断。
    IDLE:
    0: 没有检测到线路空闲
    1: 检测到线路空闲
  2. 逻辑
    1. main:
      1. 开串口接收的DMA(设置通道长度 即每次搬移的数据)
      2. 开串口空闲中断
    2. 中断函数:
      1. 判断是否是串口空闲中断
      2. 关闭DMA通道 空闲中断清除
      3. 计算DMA传输的实际长度(通道长度 - 剩余数量 = 实际传输的数量) DMA_CNDTRx:DMA通道传输量寄存器
      4. 开启串口DMA发送数据 ,将数据发送到串口工具
      5. 开启DMA接收通道(设置通道长度)
    3. 需要的函数
      HAL_UART_Receive_DMA 开启DMA
      __HAL_UART_ENABLE_IT 开启串口空闲中断
      __HAL_UART_GET_FLAG 获得串口空闲中断标志
      __HAL_UART_CLEAR_FLAG 清除串口中断标志
    4. 函数编写
int fputc(int ch,FILE *file)
{
		while (!(USART1->ISR & 1<<7));
		USART1->TDR = ch;
		return ch;
}

你可能感兴趣的:(stm32)