stm32开发笔记

发布时间 2023-09-26 22:27:02作者: ⭐⭐-fighting⭐⭐

STM32F103C8T6单片机简介

标准库与HAL库区别

  1. 寄存器

    寄存器众多,需要经常翻阅芯片手册,费时费力;

    更大灵活性,可以随心所欲达到自己的目的;

    深入理解单片机的运行原理,知其然更知其所以然。

  2. 标准库

    将寄存器底层操作都封装起来,提供一整套接口(API)供开发者调用 每款芯片都编写了一份库文件,也就是工程文件里stm32F1xx…之类的;

    配置结构体变量成员就可以修改外设的配置寄存器,从而选择不同的功能;

    大大降低单片机开发难度,但是在不同芯片间不方便移植。

  3. HAL库

    ST公司目前主力推的开发方式,新的芯片已经不再提供标准库;

    为了实现在不同芯片之间移植代码;

    为了兼容所有芯片,导致代码量庞大,执行效率低下。

通用输入输出端口GPIO

GPIO是通用输入输出端口的简称,简单来说就是STM32可控制的引脚STM32芯片的GPIO引脚与 外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能。 简单来说我们可以控制GPIO引脚的电平变化,达到我们的各种目的。

image

点亮一颗LED灯

常用的GPIO HAL库函数:

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

结构体 GPIO_InitTypeDef 定义:

typedef struct
{
uint32_t Pin;
uint32_t Mode;
uint32_t Pull;
uint32_t Speed;
} GPIO_InitTypeDef;

按键点亮LED灯(轮询法)

输入(按键):

KEY1:PA0

KEY2:PA1

输出(LED灯):

LED1:PB8

LED2:PB9

#define KEY_ON 0
#define KEY_OFF 1
uint8_t Key_Scan(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin)
{
if( HAL_GPIO_ReadPin(GPIOx,GPIO_Pin) == GPIO_PIN_RESET)
{
/* 按键按下 */
return KEY_ON;
}
else
{
/* 按键松开 */
while(HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET);
return KEY_OFF;
}
}
while (1)
{
/* USER CODE END WHILE */
if(Key_Scan(GPIOA,GPIO_PIN_0) == KEY_ON)
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_8);
if(Key_Scan(GPIOA,GPIO_PIN_1) == KEY_ON)
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_9);
/* USER CODE BEGIN 3 */
}

复位和时钟控制(RCC)

复位

  • 系统复位

当发生以下任一事件时,产生一个系统复位:

1、NRST引脚上的低电平(外部复位)

2、窗口看门狗计数终止(WWDG复位)

3、独立看门狗计数终止(IWDG复位)

4、软件复位(SW复位)

5、低功耗管理复位

  • 电源复位

当以下事件中之一发生时,产生电源复位:

1.上电/掉电复位(POR/PDR复位)

2.从待机模式中返回

  • 备份区复位

备份区域拥有两个专门的复位,它们只影响备份区域。 当以下事件中之一发生时,产生备份区域复位。

1、软件复位,备份区域复位可由设置备份域控制寄存器 (RCC_BDCR)(见6.3.9节)中的 BDRST位产生。

2、在VDD和VBAT两者掉电的前提下,VDD或VBAT上电将引发备份区域复位。

时钟控制

  • 什么是时钟?

时钟打开,对应的设备才会工作。

  • 时钟来源
  1. 三种不同的时钟源可被用来驱动系统时钟(SYSCLK)

HSI振荡器时钟(高速内部时钟)

HSE振荡器时钟(高速外部时钟)

PLL时钟(锁相环倍频时钟)

  1. 二级时钟源:

40kHz低速内部RC(LSIRC)振荡器

32.768kHz低速外部晶体(LSE晶体)

中断概述

什么是中断?

中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的 程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。

什么是EXTI?

外部中断/事件控制器(EXTI)管理了控制器的 23 个中断/事件线。每个中断/事件线都对应有一 个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。 EXTI 可以实现对每个中断/事 件线进行单独配置,可以单独配置为中断或者事件,以及触发事件的属性。

EXTI 可分为两大部分功能,一个是产生中断,另一个是产生事件,这两个功能从硬件上就有所不 同。

产生中断线路目的是把输入信号输入到 NVIC,进一步会运行中断服务函数,实现功能,这样是软 件级的。而产生事件线路目的就是传输一个脉冲信号给其他外设使用,并且是电路级别的信号传 输,属于硬件级的

EXTI初始化结构体:

typedef struct
{
//中断/事件线
uint32_t EXTI_Line; /*!< Specifies the EXTI lines to be enabled or
disabled.
This parameter can be any combination value
of @ref EXTI_Lines */
//EXTI 模式
EXTIMode_TypeDef EXTI_Mode; /*!< Specifies the mode for the EXTI lines.
This parameter can be a value of @ref
EXTIMode_TypeDef */
//触发类型
EXTITrigger_TypeDef EXTI_Trigger; /*!< Specifies the trigger signal active edge for
the EXTI lines.
This parameter can be a value of @ref
EXTITrigger_TypeDef */
//EXTI 控制
FunctionalState EXTI_LineCmd; /*!< Specifies the new state of the selected EXTI
lines.
This parameter can be set either to ENABLE or
DISABLE */
}EXTI_InitTypeDef;

中断/事件线:

#define EXTI_Line0 ((uint32_t)0x00001) /*!< External interrupt line 0 */
#define EXTI_Line1 ((uint32_t)0x00002) /*!< External interrupt line 1 */
#define EXTI_Line2 ((uint32_t)0x00004) /*!< External interrupt line 2 */
#define EXTI_Line3 ((uint32_t)0x00008) /*!< External interrupt line 3 */
#define EXTI_Line4 ((uint32_t)0x00010) /*!< External interrupt line 4 */
#define EXTI_Line5 ((uint32_t)0x00020) /*!< External interrupt line 5 */
#define EXTI_Line6 ((uint32_t)0x00040) /*!< External interrupt line 6 */
#define EXTI_Line7 ((uint32_t)0x00080) /*!< External interrupt line 7 */
#define EXTI_Line8 ((uint32_t)0x00100) /*!< External interrupt line 8 */
#define EXTI_Line9 ((uint32_t)0x00200) /*!< External interrupt line 9 */
#define EXTI_Line10 ((uint32_t)0x00400) /*!< External interrupt line 10 */
#define EXTI_Line11 ((uint32_t)0x00800) /*!< External interrupt line 11 */
#define EXTI_Line12 ((uint32_t)0x01000) /*!< External interrupt line 12 */
#define EXTI_Line13 ((uint32_t)0x02000) /*!< External interrupt line 13 */
#define EXTI_Line14 ((uint32_t)0x04000) /*!< External interrupt line 14 */
#define EXTI_Line15 ((uint32_t)0x08000) /*!< External interrupt line 15 */
#define EXTI_Line16 ((uint32_t)0x10000) /*!< External interrupt line 16
Connected to the PVD Output */
#define EXTI_Line17 ((uint32_t)0x20000) /*!< External interrupt line 17
Connected to the RTC Alarm event */
#define EXTI_Line18 ((uint32_t)0x40000) /*!< External interrupt line 18
Connected to the USB OTG FS Wakeup from suspend event */
#define EXTI_Line19 ((uint32_t)0x80000) /*!< External interrupt line 19
Connected to the Ethernet Wakeup event */
#define EXTI_Line20 ((uint32_t)0x00100000) /*!< External interrupt line 20
Connected to the USB OTG HS (configured in FS) Wakeup event */
#define EXTI_Line21 ((uint32_t)0x00200000) /*!< External interrupt line 21
Connected to the RTC Tamper and Time Stamp events */
#define EXTI_Line22 ((uint32_t)0x00400000) /*!< External interrupt line 22
Connected to the RTC Wakeup event */

EXTI模式:

typedef enum
{
EXTI_Mode_Interrupt = 0x00, //产生中断
EXTI_Mode_Event = 0x04 //产生事件
}EXTIMode_TypeDef;

触发类型:

typedef enum
{
EXTI_Trigger_Rising = 0x08, //上升沿
EXTI_Trigger_Falling = 0x0C, //下降沿
EXTI_Trigger_Rising_Falling = 0x10 //上升沿和下降沿都触发
}EXTITrigger_TypeDef;

EXTI控制:

使能 EXTI ,一般都是使能, ENABLE

什么是优先级?

抢占优先级和响应优先级的区别:

  • 高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的。

  • 抢占优先级相同的中断,高响应优先级不可以打断低响应优先级的中断。

  • 抢占优先级相同的中断,当两个中断同时发生的情况下,哪个响应优先级高,哪个先执行。

  • 如果两个中断的抢占优先级和响应优先级都是一样的话,则看哪个中断先发生就先执行

什么是优先级分组?

Cortex-M3允许具有较少中断源时使用较少的寄存器位指定中断源的优先级,因此STM32把 指定中断优先级的寄存器位减少到4位,这4个寄存器位的分组方式如下:

  • 第0组:所有4位用于指定响应优先级
  • 第1组:最高1位用于指定抢占式优先级,最低3位用于指定响应优先级
  • 第2组:最高2位用于指定抢占式优先级,最低2位用于指定响应优先级
  • 第3组:最高3位用于指定抢占式优先级,最低1位用于指定响应优先级
  • 第4组:所有4位用于指定抢占式优先级

什么是NVIC?

STM32通过中断控制器NVIC(Nested Vectored Interrupt Controller)进行中断的管理 。 NVIC是属于Cortex内核的器件,不可屏蔽中断(NMI)和外部中断都由它来处理,但是SYSTICK 不是由NVIC控制的

typedef struct
{
uint8_t NVIC_IRQChannel;
uint8_t NVIC_IRQChannelPreemptionPriority; //抢断优先级
uint8_t NVIC_IRQChannelSubPriority; //响应优先级
FunctionalState NVIC_IRQChannelCmd;
} NVIC_InitTypeDef;

什么是中断向量表?

每个中断源都有对应的处理程序,这个处理程序称为中断服务程序,其入口地址称为中断向 量。所有中断的中断服务程序入口地址构成一个表,称为中断向量表;也有的机器把中断服务程 序入口的跳转指令构成一张表,称为中断向量跳转表。

按键点亮LED灯(中断法)

  1. 配置时钟
  2. 配置GPIO口
  3. 使能中断
  4. 配置工程

项目一:电动车报警器

项目需求

点击遥控器 A 按键,系统进入警戒模式,一旦检测到震动(小偷偷车),则喇叭发出声响报警, 吓退小偷。

点击遥控器 B 按键,系统退出警戒模式,再怎么摇晃系统都不会报警,否则系统一直发出尖叫, 让车主尴尬。

振动传感器介绍

1、 产品不震动,输出高电平

2、产品震动,输出低电平,绿色指示灯亮

需求:当振动传感器接收到振动信号时,使用中断方式点亮LED1。

// 重写中断服务函数,如果检测到EXTI中断请求,则进入此函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    // 一根中断线上接有多个中断源,判断中断请求是否来自PA4
    if (GPIO_Pin == GPIO_PIN_4)
    {
        // 如果检测到PA4被拉低
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_4) == GPIO_PIN_RESET)
        {
            // 则点亮LED1
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);
            // 延时1秒
            HAL_Delay(1000);
            // 关闭LED1
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
        }
        else
        {
            // 未检测到PA4被拉低,则关闭LED1
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
        }
    }
}

重点

如果直接在中断服务函数里调用 HAL_Delay 函数,则会造成系统卡死。

原因:程序初始化时默认把滴答定时器的中断优先级设为最低,其它中断源很容易打断它导致卡 死。

解决:在 main 函数里使用以下函数提高滴答定时器的中断优先级(提升至0):

HAL_NVIC_SetPriority(SysTick_IRQn,0,0);

并且将 EXTI4 的中断优先级设置比滴答定时器的中断优先级高,比如 2 。

需求:按下遥控器A按键,LED1亮1秒;按下遥控器B按键,LED2亮1秒。

// 重写中断服务函数,如果检测到EXTI中断请求,则进入此函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    switch (GPIO_Pin)
    {
    // 如果检测到PA5被拉高(按键A被按下)
    case GPIO_PIN_5:
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_SET)
        {
            // 则点亮LED1
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);
            HAL_Delay(1000);
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
        }
        else
        {
            // 如果未检测到PA5,则关闭LED1
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
        }
        break;
    // 如果检测到PA6被拉高(按键B按下)
    case GPIO_PIN_6:
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6) == GPIO_PIN_SET)
        {
            // 则点亮LED2
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET);
            HAL_Delay(1000);
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET);
        }
        else
        {
            // 如果未检测到PA4,则关闭LED1
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET);
        }
        break;
    }
}

项目设计

//如果检测到PA4被拉低(小偷偷车),并且警报模式打开 //则将PB7拉低,继电器通电,喇叭一直响 // 如果检测到PA5被拉高(按键A按下),设定为开启警报模式 // 则将PB7拉低(喇叭响),2秒后恢复电平(喇叭不响),表示进入警报模式 // 同时将标志位设置为ON // 如果检测到PA6被拉高(按键B按下),设定为关闭警报模式 // 则将PB7拉低(喇叭响),1秒后恢复电平(喇叭不响),表示关闭警报模式 // 同时将标志位设置为OFF

#define J_ON 1
#define J_OFF 0
// 重写中断服务函数,如果检测到EXTI中断请求,则进入此函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    static int mark = J_OFF;
    switch (GPIO_Pin)
    {
    case GPIO_PIN_4:
        // 如果检测到PA4被拉低(小偷偷车),并且警报模式打开
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_4) == GPIO_PIN_RESET && mark == J_ON)
        {
            // 则将PB7拉低,继电器通电,喇叭一直响
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);
        }
        break;
    // 如果检测到PA5被拉高(按键A按下),设定为开启警报模式
    case GPIO_PIN_5:
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_SET)
        {
            // 则将PB7拉低(喇叭响),2秒后恢复电平(喇叭不响),表示进入警报模式
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);
            HAL_Delay(2000);
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
            // 同时将标志位设置为ON
            mark = J_ON;
        }
        break;
    // 如果检测到PA6被拉高(按键B按下),设定为关闭警报模式
    case GPIO_PIN_6:
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6) == GPIO_PIN_SET)
        {
            // 则将PB7拉低(喇叭响),1秒后恢复电平(喇叭不响),表示关闭警报模式
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);
            HAL_Delay(1000);
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
            // 同时将标志位设置为OFF
            mark = J_OFF;
        }
        break;
    }
}

定时器Timer

定时器分类:

  • 基本定时器(TIM6~TIM7)
  • 通用定时器(TIM2~TIM5)
  • 高级定时器(TIM1和TIM8)

image

通用定时器介绍:

image

定时器计数模式:

image

定时器溢出时间计算公式:

image

定时器中断实验

需求:使用定时器中断方法,每500ms翻转一次LED1灯状态。

  1. RCC配置
  2. LED1灯配置
  3. 时钟数配置
  4. TIM2配置

image

重写更新中断回调函数:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
        HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_8);
}

启动定时器

在main.c中,在定时器初始化命令之后加入以下代码

HAL_TIM_Base_Start_IT(&htim2);

PWM介绍

STM32F103C8T6 PWM资源:

  • 高级定时器(TIM1):7路
  • 通用定时器(TIM2~TIM4):各4路

PWM输出模式:

  • PWM模式1:在向上计数时,一旦 CNT < CCRx 时输出为有效电平,否则为无效电平; 在向 下计数时,一旦 CNT > CCRx 时输出为无效电平,否则为有效电平。
  • PWM模式2:在向上计数时,一旦 CNT < CCRx 时输出为无效电平,否则为有效电平; 在向 下计数时,一旦 CNT > CCRx 时输出为有效电平,否则为无效电平。

PWM周期与频率:

image

PWM占空比:

由TIMx_CCRx寄存器决定。

PWM实验

需求:使用PWM点亮LED1实现呼吸灯效果

LED灯为什么可以越来越亮,越来越暗?

这是由不同的占空比决定的。

如何计算周期/频率?

假如频率为 2kHz ,则:PSC=71,ARR=499

LED1连接到哪个定时器的哪一路?

学会看产品手册

  1. 设置时钟

  2. 设置定时器

image

// 定义变量
uint16_t pwmVal = 0; // 调整PWM占空比
uint8_t dir = 1;     // 设置改变方向。1:占空比越来越大;0:占空比越来越小
// 使能 Timer4 第3通道 PWM 输出
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_3);
// while循环实现呼吸灯效果
while (1)
{
    HAL_Delay(1);
    if (dir)
        pwmVal++;
    else
        pwmVal--;
    if (pwmVal > 500)
        dir = 0;
    if (pwmVal == 0)
        dir = 1;
    // 修改比较值,修改占空比
    __HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_3, pwmVal);
}

项目二:感应开关盖垃圾桶

项目需求:

  • 检测靠近时,垃圾桶自动开盖并伴随滴一声,2秒后关盖
  • 发生震动时,垃圾桶自动开盖并伴随滴一声,2秒后关盖
  • 按下按键时,垃圾桶自动开盖并伴随滴一声,2秒后关盖

sg90舵机介绍及实战

PWM波的频率不能太高,大约50HZ,即周期=1/频率=1/50=0.02s,20ms左右。

image

如果周期为20ms,则 PSC=7199,ARR=199

角度控制

  • 0.5ms-------------0度; 2.5% 对应函数中CCRx为5
  • 1.0ms------------45度; 5.0% 对应函数中CCRx为10
  • 1.5ms------------90度; 7.5% 对应函数中CCRx为15
  • 2.0ms-----------135度; 10.0% 对应函数中CCRx为20
  • 2.5ms-----------180度; 12.5% 对应函数中CCRx为25

每隔1s,转动一个角度:0度 --> 45度 --> 90度 --> 135度 --> 180度 --> 0度

HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_3);
while (1)
{
    HAL_Delay(1000);
    __HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_3, 5);
    HAL_Delay(1000);
    __HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_3, 10);
    HAL_Delay(1000);
    __HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_3, 15);
    HAL_Delay(1000);
    __HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_3, 20);
    HAL_Delay(1000);
    __HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_3, 25);
}

超声波传感器介绍

使用超声波测距,当手离传感器距离小于5cm时,LED1点亮,否则保持不亮状态。

image

重点定时器作微妙级延时函数

// 使用TIM2来做us级延时函数
void TIM2_Delay_us(uint16_t n_us)
{
    /* 使能定时器2计数 */
    __HAL_TIM_ENABLE(&htim2);
    __HAL_TIM_SetCounter(&htim2, 0);
    while (__HAL_TIM_GetCounter(&htim2) < ((1 * n_us) - 1))
        ;
    /* 关闭定时器2计数 */
    __HAL_TIM_DISABLE(&htim2);
}

整体项目

int cnt;
float distance;
while (1)
{
    // 1. Trig ,给Trig端口至少10us的高电平
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); // 拉高
    TIM2_Delay_us(20);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); // 拉低
    // 2. echo由低电平跳转到高电平,表示开始发送波
    // 波发出去的那一下,开始启动定时器
    while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) == GPIO_PIN_RESET)
        ; // 等待输入电平拉高
    HAL_TIM_Base_Start(&htim2);
    __HAL_TIM_SetCounter(&htim2, 0);
    // 3. 由高电平跳转回低电平,表示波回来了
    while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) == GPIO_PIN_SET)
        ; // 等待输入电平变低
    // 波回来的那一下,我们开始停止定时器
    HAL_TIM_Base_Stop(&htim2);
    // 4. 计算出中间经过多少时间
    cnt = __HAL_TIM_GetCounter(&htim2);
    // 5. 距离 = 速度 (340m/s)* 时间/2(计数1次表示1us)
    distance = cnt * 340 / 2 * 0.000001 * 100; // 单位:cm
    if (distance < 5)
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);
    else
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
    // 每500毫秒测试一次距离
    HAL_Delay(500);
}

串口

串口发送/接受函数

HAL_UART_Transmit(); 串口发送数据,使用超时管理机制

HAL_UART_Receive(); 串口接收数据,使用超时管理机制

HAL_UART_Transmit_IT(); 串口中断模式发送

HAL_UART_Receive_IT(); 串口中断模式接收

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart,
uint8_t *pData, uint16_t Size, uint32_t Timeout)

作用:以阻塞的方式发送指定字节的数据

  • 形参 1 :UART_HandleTypeDef 结构体类型指针变量
  • 形参 2:指向要发送的数据地址
  • 形参 3:要发送的数据大小,以字节为单位
  • 形参 4:设置的超时时间,以ms单位
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart,
uint8_t *pData, uint16_t Size)

作用:以中断的方式接收指定字节的数据

  • 形参 1 是 UART_HandleTypeDef 结构体类型指针变量
  • 形参 2 是指向接收数据缓冲区
  • 形参 3 是要接收的数据大小,以字节为单位

此函数执行完后将清除中断,需要再次调用以重新开启中断。

串口中断回调函数:

  • HAL_UART_IRQHandler(UART_HandleTypeDef *huart); //串口中断处理函数
  • HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); //发送中断回调函数
  • HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart); //接收中断回调函数

状态标记变量: USART_RX_STA

从0开始,串口中断接收到一个数据(一个字节)就自增1。当数据读取全部OK时候(回车和换行 符号来的时候),那么 USART_RX_STA的最高位置1,表示串口数据接收全部完毕了,然后main 函数里面可以处理数据了。

image

串口接收中断流程

image

串口实验(非中断)

#include <stdio.h>
#include <string.h>
unsigned char ch[20] = {0};
int fputc(int ch, FILE *f)
{
    unsigned char temp[1] = {ch};
    HAL_UART_Transmit(&huart1, temp, 1, 0xffff);
    return ch;
}
// main函数里: 
unsigned char ch[20] = {0};
HAL_UART_Transmit(&huart1, "hello world\n", strlen("hello world\n"), 100);
while (1)
{
    HAL_UART_Receive(&huart1, ch, 19, 100);
    // HAL_UART_Transmit(&huart1, ch, strlen(ch), 100);
    printf(ch);
    memset(ch, 0, strlen(ch));
}

串口实验(中断)

#include <stdio.h>
// 串口接收缓存(1字节)
uint8_t buf = 0;
// 定义最大接收字节数 200,可根据需求调整
#define UART1_REC_LEN 200
// 接收缓冲, 串口接收到的数据放在这个数组里,最大UART1_REC_LEN个字节
uint8_t UART1_RX_Buffer[UART1_REC_LEN];
// 接收状态
// bit15, 接收完成标志
// bit14, 接收到0x0d
// bit13~0, 接收到的有效字节数目
uint16_t UART1_RX_STA = 0;
// 接收完成回调函数,收到一个数据后,在这里处理
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 判断中断是由哪个串口触发的
    if (huart->Instance == USART1)
    {
        // 判断接收是否完成(UART1_RX_STA bit15 位是否为1)
        if ((UART1_RX_STA & 0x8000) == 0)
        {
            // 如果已经收到了 0x0d (回车),
            if (UART1_RX_STA & 0x4000)
            {
                // 则接着判断是否收到 0x0a (换行)
                if (buf == 0x0a)
                    // 如果 0x0a 和 0x0d 都收到,则将 bit15 位置为1
                    UART1_RX_STA |= 0x8000;
                else
                    // 否则认为接收错误,重新开始
                    UART1_RX_STA = 0;
            }
            else // 如果没有收到了 0x0d (回车)
            {
                // 则先判断收到的这个字符是否是 0x0d (回车)
                if (buf == 0x0d)
                {
                    // 是的话则将 bit14 位置为1
                    UART1_RX_STA |= 0x4000;
                }
                else
                {
                    // 否则将接收到的数据保存在缓存数组里
                    UART1_RX_Buffer[UART1_RX_STA & 0X3FFF] = buf;
                    UART1_RX_STA++;
                    // 如果接收数据大于UART1_REC_LEN(200字节),则重新开始接收
                    if (UART1_RX_STA > UART1_REC_LEN - 1)
                        UART1_RX_STA = 0;
                }
            }
        }
        // 重新开启中断
        HAL_UART_Receive_IT(&huart1, &buf, 1);
    }
}
int fputc(int ch, FILE *f)
{
    unsigned char temp[1] = {ch};
    HAL_UART_Transmit(&huart1, temp, 1, 0xffff);
    return ch;
}
// main函数部分
HAL_UART_Receive_IT(&huart1, &buf, 1);
while (1)
{
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
    // 判断判断串口是否接收完成
    if (UART1_RX_STA & 0x8000)
    {
        printf("收到数据:");
        // 将收到的数据发送到串口
        HAL_UART_Transmit(&huart1, UART1_RX_Buffer, UART1_RX_STA & 0x3fff, 0xffff);
        // 等待发送完成
        while (huart1.gState != HAL_UART_STATE_READY)
            ;
        printf("\r\n");
        // 重新开始下一次接收
        UART1_RX_STA = 0;
    }
    printf("hello liangxu\r\n");
    HAL_Delay(1000);
}

独立看门狗

​ 在由单片机构成的微型计算机系统中,由于单片机的工作常常会受到来自外界电磁场的干扰,造 成程序的跑飞,而陷入死循环,程序的正常运行被打断,由单片机控制的系统无法继续工作,会 造成整个系统的陷入停滞状态,发生不可预料的后果,所以出于对单片机运行状态进行实时监测 的考虑,便产生了一种专门用于监测单片机程序运行状态的模块或者芯片,俗称“看门狗” (watchdog)

独立看门狗工作在主程序之外,能够完全独立工作,它的时钟是专用的低速时钟(LSI),由 VDD 电压供电, 在停止模式和待机模式下仍能工作。

独立看门狗本质

本质是一个 12 位的递减计数器,当计数器的值从某个值一直减到0的时候,系统就会产生一个复 位信号,即 IWDG_RESET 。

如果在计数没减到0之前,刷新了计数器的值的话,那么就不会产生复位信号,这个动作就是我们 经常说的喂狗。

独立看门狗时钟

独立看门狗的时钟由独立的RC振荡器LSI提供,即使主时钟发生故障它仍然有效,非常独立。启用 IWDG后,LSI时钟会自动开启。LSI时钟频率并不精确,F1用40kHz。 LSI经过一个8位的预分频器得到计数器时钟。

重装载寄存器

重装载寄存器是一个12位的寄存器,用于存放重装载值,低12位有效,即最大值为4096,这个值 的大小决定着独立看门狗的溢出时间

键寄存器

键寄存器IWDG_KR可以说是独立看门狗的一个控制寄存器,主要有三种控制方式,往这个寄存器 写入下面三个不同的值有不同的效果

独立看门狗实验

需求:开启独立看门狗,溢出时间为1秒,使用按键1进行喂狗。

#include <string.h>
// main函数:
HAL_UART_Transmit(&huart1, "程序启动。。\n", strlen("程序启动。。\n"), 100);
while (1)
{
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
    if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
        HAL_IWDG_Refresh(&hiwdg);
    HAL_Delay(50);
}

窗口看门狗

窗口看门狗用于监测单片机程序运行时效是否精准,主要检测软件异常,一般用于需要精准检测 程序运行时间的场合。

窗口看门狗的本质是一个能产生系统复位信号提前唤醒中断的6位计数器。 产生复位条件:

  • 当递减计数器值从 0x40 减到 0x3F 时复位(即T6位跳变到0)
  • 计数器的值大于 W[6:0] 值时喂狗会复位。

产生中断条件:

  • 当递减计数器等于 0x40 时可产生提前唤醒中断 (EWI)。

在窗口期内重装载计数器的值,防止复位,也就是所谓的喂狗。

两者之间的区别

image-20230807104503176

DMA(数据搬运工)

DMA控制器

STM32F103有2个 DMA 控制器,DMA1有7个通道,DMA2有5个通道。

一个通道每次只能搬运一个外设的数据!! 如果同时有多个外设的 DMA 请求,则按照优先级进 行响应。

优先级管理采用软件+硬件:

  • 软件: 每个通道的优先级可以在DMA_CCRx寄存器中设置,有4个等级 最高级>高级>中级>低级
  • 硬件: 如果2个请求,它们的软件优先级相同,则较低编号的通道比较高编号的通道有较高 的优先权。 比如:如果软件优先级相同,通道2优先于通道4

DMA传输方式

  • DMA_Mode_Normal(正常模式) 一次DMA数据传输完后,停止DMA传送 ,也就是只传输一次
  • DMA_Mode_Circular(循环传输模式) 当传输结束时,硬件自动会将传输数据量寄存器进行重装,进行下一轮的数据传输。 也就是 多次传输模式

指针递增模式 外设和存储器指针在每次传输后可以自动向后递增或保持常量。当设置为增量模式时,下一个要 传输的地址将是前一个地址加上增量值。

image

实验一、内存到内存搬运

用到的库函数

  1. HAL_DMA_Star

    HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t
    DstAddress, uint32_t DataLength)
    

    参数一:DMA_HandleTypeDef *hdma,DMA通道句柄

    参数二:uint32_t SrcAddress,源内存地址

    参数三:uint32_t DstAddress,目标内存地址

    参数四:uint32_t DataLength,传输数据长度。注意:需要乘以sizeof(uint32_t)

    返回值:HAL_StatusTypeDef,HAL状态(OK,busy,ERROR,TIMEOUT)

  2. __HAL_DMA_GET_FLAG

    #define __HAL_DMA_GET_FLAG(__HANDLE__, __FLAG__) (DMA1->ISR & (__FLAG__))
    

    参数一:HANDLE,DMA通道句柄

    参数二:FLAG,数据传输标志。DMA_FLAG_TCx表示数据传输完成标志

    返回值:FLAG的值(SET/RESET)

代码实现

  1. 开启数据传输
  2. 等待数据传输完成
  3. 打印数组内容
#define BUF_SIZE 16
// 源数组
uint32_t srcBuf[BUF_SIZE] = {
    0x00000000, 0x11111111, 0x22222222, 0x33333333,
    0x44444444, 0x55555555, 0x66666666, 0x77777777,
    0x88888888, 0x99999999, 0xAAAAAAAA, 0xBBBBBBBB,
    0xCCCCCCCC, 0xDDDDDDDD, 0xEEEEEEEE, 0xFFFFFFFF};
// 目标数组
uint32_t desBuf[BUF_SIZE];
int fputc(int ch, FILE *f)
{
    unsigned char temp[1] = {ch};
    HAL_UART_Transmit(&huart1, temp, 1, 0xffff);
    return ch;
}
// main函数里:
// 开启数据传输
HAL_DMA_Start(&hdma_memtomem_dma1_channel1,
              (uint32_t)srcBuf, (uint32_t)desBuf, sizeof(uint32_t) * BUF_SIZE);
// 等待数据传输完成
while (__HAL_DMA_GET_FLAG(&hdma_memtomem_dma1_channel1, DMA_FLAG_TC1) == RESET)
    ;
// 打印数组内容
for (i = 0; i < BUF_SIZE; i++)
    printf("Buf[%d] = %X\r\n", i, desBuf[i]);

实验二、内存到外设搬运

用到的库函数

HAL_UART_Transmit_DMA

HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData,
uint16_t Size)

参数一:UART_HandleTypeDef *huart,串口句柄

参数二:uint8_t *pData,待发送数据首地址

参数三:uint16_t Size,待发送数据长度

返回值:HAL_StatusTypeDef,HAL状态(OK,busy,ERROR,TIMEOUT)

代码实现

  1. 准备数据
  2. 将数据通过串口DMA发送
#define BUF_SIZE 1000
// 待发送的数据
unsigned char sendBuf[BUF_SIZE];
main函数里
    // 准备数据
    for (i = 0; i < BUF_SIZE; i++)
        sendBuf[i] = 'A';
// 将数据通过串口DMA发送
HAL_UART_Transmit_DMA(&huart1, sendBuf, BUF_SIZE);
while (1)
{
    HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_8);
    HAL_Delay(100);
}

实验三、外设到内存搬运

用到的库函数

  1. __HAL_UART_ENABLE
#define __HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__) ((((__INTERRUPT__) >> 28U)
== UART_CR1_REG_INDEX)? ((__HANDLE__)->Instance->CR1 |= ((__INTERRUPT__) &
UART_IT_MASK)): \
(((__INTERRUPT__) >> 28U)
== UART_CR2_REG_INDEX)? ((__HANDLE__)->Instance->CR2 |= ((__INTERRUPT__) &
UART_IT_MASK)): \
((__HANDLE__)->Instance-
>CR3 |= ((__INTERRUPT__) & UART_IT_MASK)))

参数一:HANDLE,串口句柄

参数二:INTERRUPT,需要使能的中断

返回值:无

  1. HAL_UART_Receive_DMA

    HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData,
    uint16_t Size)
    

    参数一:UART_HandleTypeDef *huart,串口句柄

    参数二:uint8_t *pData,接收缓存首地址

    参数三:uint16_t Size,接收缓存长度

    返回值:HAL_StatusTypeDef,HAL状态(OK,busy,ERROR,TIMEOUT)

  2. __HAL_UART_GET_FLAG

#define __HAL_UART_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->Instance->SR &
(__FLAG__)) == (__FLAG__))

参数一:HANDLE,串口句柄

参数二:FLAG,需要查看的FLAG

返回值:FLAG的值

  1. __HAL_UART_CLEAR_IDLEFLAG
#define __HAL_UART_CLEAR_IDLEFLAG(__HANDLE__) __HAL_UART_CLEAR_PEFLAG(__HANDLE__)

参数一:HANDLE,串口句柄

返回值:无

  1. HAL_UART_DMAStop
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart)

参数一:UART_HandleTypeDef *huart,串口句柄

返回值:HAL_StatusTypeDef,HAL状态(OK,busy,ERROR,TIMEOUT)

  1. __HAL_DMA_GET_COUNTER
#define __HAL_DMA_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->CNDTR)

参数一:HANDLE,串口句柄

返回值:未传输数据大小

代码实现

如何判断串口接收是否完成?如何知道串口收到数据的长度?

使用串口空闲中断(IDLE)!

串口空闲时,触发空闲中断;

空闲中断标志位由硬件置1,软件清零

利用串口空闲中断,可以用如下流程实现DMA控制的任意长数据接收:

  1. 使能IDLE空闲中断;
  2. 使能DMA接收中断;
  3. 收到串口接收中断,DMA不断传输数据到缓冲区;
  4. 一帧数据接收完毕,串口暂时空闲,触发串口空闲中断;
  5. 在中断服务函数中,清除中断标志位,关闭DMA传输(防止干扰);
  6. 计算刚才收到了多少个字节的数据。
  7. 处理缓冲区数据,开启DMA传输,开始下一帧接收。
有三个文件需要修改
main.c
uint8_t rcvBuf[BUF_SIZE];                    // 接收数据缓存数组
uint8_t rcvLen = 0;                          // 接收一帧数据的长度
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 使能IDLE空闲中断
HAL_UART_Receive_DMA(&huart1, rcvBuf, 100);  // 使能DMA接收中断
while (1)
{
    HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_8);
    HAL_Delay(300);
}
main.h
#define BUF_SIZE 100
stm32f1xx_it.c
extern uint8_t rcvBuf[BUF_SIZE];
extern uint8_t rcvLen;
void USART1_IRQHandler(void)
{
    /* USER CODE BEGIN USART1_IRQn 0 */
    /* USER CODE END USART1_IRQn 0 */
    HAL_UART_IRQHandler(&huart1);
    /* USER CODE BEGIN USART1_IRQn 1 */
    if ((__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) == SET)) // 判断IDLE标志位是否被置位
    {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1); // 清除标志位
        HAL_UART_DMAStop(&huart1);          // 停止DMA传输,防止干扰
        uint8_t temp = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
        rcvLen = BUF_SIZE - temp;                        // 计算数据长度
        HAL_UART_Transmit_DMA(&huart1, rcvBuf, rcvLen);  // 发送数据
        HAL_UART_Receive_DMA(&huart1, rcvBuf, BUF_SIZE); // 开启DMA
    }
    /* USER CODE END USART1_IRQn 1 */
}

ADC(模拟/数字转换器)

ADC特性

12位精度下转换速度可高达1MHZ

采样时间可配置,采样时间越长, 转换结果相对越准确, 但是转换速度就越慢

ADC 的结果可以左对齐或右对齐方式存储在 16 位数据寄存器中

ADC的性能指标

  • 量程:能测量的电压范围
  • 分辨率:ADC能辨别的最小模拟量,通常以输出二进制数的位数表示,比如:8、10、12、 16位等;位数越多,分辨率越高,一般来说分辨率越高,转化时间越长
  • 转化时间:从转换开始到获得稳定的数字量输出所需要的时间称为转换时间

ADC通道

总共2个ADC(ADC1,ADC2),每个ADC有18个转换通道: 16个外部通道、 2个内部通道(温度 传感器、内部参考电压)

ADC转换顺序

每个ADC只有一个数据寄存器,16个通道一起共用这个寄存器,所以需要指定规则转换通道的转 换顺序。

ADC转化时间

ADC是挂载在APB2总线(PCLK2)上的,经过分频器得到ADC时钟(ADCCLK),最高 14 MHz

image

12.5个周期是固定的,一般我们设置 PCLK2=72M,经过 ADC 预分频器能分频到最大的时钟只能 是 12M,采样周期设置为 1.5 个周期,算出最短的转换时间为 1.17us。

ADC转化模式

扫描模式

关闭扫描模式:只转换ADC_SQRx或ADC_JSQR选中的第一个通道

打开扫描模式:扫描所有被ADC_SQRx或ADC_JSQR选中的所有通道

单次转换/连续转换

单次转换:只转换一次

连续转换:转换一次之后,立马进行下一次转换

while (1)
{
    HAL_ADC_Start(&hadc1);                  // 启动ADC单次转换
    HAL_ADC_PollForConversion(&hadc1, 50);  // 等待ADC转换完成
    smoke_value = HAL_ADC_GetValue(&hadc1); // 读取ADC转换数据
    printf("smoke_value = %f\r\n", 3.3 / 4096 * smoke_value);
    //最大电压为3.3v,最大的刻度为2^12
    // printf("smoke_value = %d \r\n", smoke_value);
    HAL_Delay(500);
}

IIC

用到的库函数:

HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c,
                                    uint16_t DevAddress,
                                    uint16_t MemAddress,
                                    uint16_t MemAddSize,
                                    uint8_t *pData,
                                    uint16_t Size,
                                    uint32_t Timeout)

参数一:I2C_HandleTypeDef *hi2c,I2C设备句柄

参数二:uint16_t DevAddress,目标器件的地址,七位地址必须左对齐

参数三:uint16_t MemAddress,目标器件的目标寄存器地址

参数四:uint16_t MemAddSize,目标器件内部寄存器地址数据长度

参数五:uint8_t *pData,待写的数据首地址

参数六:uint16_t Size,待写的数据长度 参数七:uint32_t Timeout,超时时间

返回值:HAL_StatusTypeDef,HAL状态(OK,busy,ERROR,TIMEOUT)

向OLED写命令的封装:

void Oled_Write_Cmd(uint8_t dataCmd)
{
HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT,
&dataCmd, 1, 0xff);
}

向OLED写数据的封装:

void Oled_Write_Data(uint8_t dataData)
{
HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT,
&dataData, 1, 0xff);
}

SPI

SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总 线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提 供方便,正是出于这种简单易用的特性,越来越多的芯片集成了这种通信协议,比如 AT91RM9200 。

SPI 包含 4 条总线,SPI 总线包含 4 条总线,分别为SS、SCK、MOSI、MISO。它们的作用介绍如 下 :

(1) MISO – Master Input Slave Output,主设备数据输入,从设备数据输出

(2) MOSI – Master Output Slave Input,主设备数据输出,从设备数据输入

(3) SCK – Serial Clock,时钟信号,由主 设备产生

(4) CS – Chip Select,片选信号,由主设备控制

SPI 工作模式

image

模式 0 和模式 3 最常用。

模式0时序图:

image

模式 3 时序图:

image

W25Q128 介绍

W25Q128 是华邦公司推出的一款 SPI 接口的 NOR Flash 芯片,其存储空间为 128 Mbit,相当于 16M 字节。 Flash 是常用的用于储存数据的半导体器件,它具有容量大,可重复擦写、按“扇区/块”擦除、掉 电后数据可继续保存的特性。 Flash 是有一个物理特性:只能写 0 ,不能写 1 ,写 1 靠擦除。

W25Q128 常用指令

image

image

image

写操作

image

w25q128_write_nocheck流程图

image

项目六、温湿度LCD显示并上传服务器

LCD1602

引脚封装

RS、RW、EN三根信号线经常需要进行拉高/拉低操作,可以进行封装

#define RS_GPIO_Port GPIOB
#define RW_GPIO_Port GPIOB
#define EN_GPIO_Port GPIOB
#define RS_Pin GPIO_PIN_1
#define RW_Pin GPIO_PIN_2
#define EN_Pin GPIO_PIN_10
#define RS_HIGH HAL_GPIO_WritePin(RS_GPIO_Port, RS_Pin, GPIO_PIN_SET)
#define RS_LOW HAL_GPIO_WritePin(RS_GPIO_Port, RS_Pin, GPIO_PIN_RESET)
#define RW_HIGH HAL_GPIO_WritePin(RW_GPIO_Port, RW_Pin, GPIO_PIN_SET)
#define RW_LOW HAL_GPIO_WritePin(RW_GPIO_Port, RW_Pin, GPIO_PIN_RESET)
#define EN_HIGH HAL_GPIO_WritePin(EN_GPIO_Port, EN_Pin, GPIO_PIN_SET)
#define EN_LOW HAL_GPIO_WritePin(EN_GPIO_Port, EN_Pin, GPIO_PIN_RESET)

如何将一个字节的数据按位一次性发送到GPIOA的8个管脚?

GPIOA->ODR = cmd;

DHT11

引脚封装

#define DHT_HIGHT HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET)
#define DHT_LOW HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET)
#define DHT_VALUE HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7)

在STM32或者许多其他嵌入式系统中,通常情况下,GPIO引脚(通用输入输出引脚)的高电平(也称为逻辑高电平)被读取为1。同样,低电平通常被读取为0

//微妙延迟函数
void delay_us(uint16_t cnt)
{
    uint8_t i;
    while (cnt)
    {
        for (i = 0; i < 10; i++)
        {
        }
        cnt--;
    }
}

项目实现

注意点:

  1. 不要忘记将Use MicroLIB的勾打上;
  2. 不要忘记在main函数把串口中断打开;
  3. 使用蓝牙模块时,记得将波特率设置为9600
char message[16];
while (1)
{
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
    Read_Data_From_DHT();
    if (datas[2] >= 25)
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET);
    else
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);
    memset(message, 0, sizeof(message));
    sprintf(message, "Temp: %d.%d", datas[2], datas[3]);
    LCD1602_showLine(1, 0, message);
    memset(message, 0, sizeof(message));
    sprintf(message, "Humi: %d.%d", datas[0], datas[1]);
    LCD1602_showLine(2, 0, message);
    HAL_Delay(1000);
}

重点:软件做微秒延时函数

void delay_us(uint16_t cnt)
{
    uint8_t i;
    while (cnt)
    {
        for (i = 0; i < 10; i++)
        {
        }
        cnt--;
    }
}

智能小车

测速小车

重点

每一秒钟打印一次小车的速度

unsigned int speedCnt;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_14)
        if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_14) == GPIO_PIN_RESET)
            speedCnt++;
}
//定时器溢出中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    printf("speed: %d\r\n", speedCnt);
    speedCnt = 0;
}
// main函数里:
HAL_TIM_Base_Start_IT(&htim2);//定时器开启的函数

%2d可以占用两个字符