STM32 HAL库实战指南:从零开始构建嵌入式应用系统
目录
- 前言:为什么选择HAL库
- [第一章:STM32 HAL库基础架构深度解析](#第一章:STM32 HAL库基础架构深度解析)
- 第二章:时钟系统:嵌入式系统的脉搏
- 第三章:GPIO编程:点亮你的第一个LED
- 第四章:中断系统:让系统更智能
- 第五章:定时器应用:精准的时间控制
- 第六章:串口通信:设备间的桥梁
- 第七章:I2C总线:多设备协同的纽带
- 第八章:SPI通信:高速数据传输
- 第九章:ADC采集:感知世界的窗口
- 第十章:DMA传输:解放CPU的利器
- 第十一章:低功耗设计:让设备更持久
- 第十二章:综合实战项目:智能环境监测系统
- 总结与展望
前言:为什么选择HAL库
在嵌入式开发的世界里,STM32系列微控制器以其强大的性能和丰富的外设资源,成为了无数工程师的首选。然而,面对如此庞大的功能集合,如何高效地进行开发?STM32 HAL库(Hardware Abstraction Layer)应运而生,它为我们提供了一套统一、易用的API接口,让复杂的硬件操作变得简单直观。
HAL库的优势
1. 跨系列兼容性
HAL库为STM32全系列(F0/F1/F3/F4/F7/H7等)提供了统一的编程接口。这意味着:
- 学习一套API,适用于多个系列
- 代码移植成本低,易于维护
- 团队协作更高效
2. 丰富的功能支持
HAL库不仅支持基本外设,还提供了:
- 高级功能(如USB、以太网、CAN等)
- 中间件支持(FreeRTOS、FatFS等)
- 完善的错误处理机制
3. 强大的工具链支持
- STM32CubeMX:图形化配置工具,自动生成初始化代码
- STM32CubeIDE:集成开发环境,一站式开发
- 丰富的例程:官方提供大量示例代码
本文档的学习路径
本文档采用"理论+实践"的方式,通过12个章节,从基础到高级,从理论到实战,带你全面掌握STM32 HAL库开发。每个章节都包含:
- 原理讲解:深入理解底层机制
- 代码示例:实际可运行的代码
- 实战项目:完整的应用案例
- 常见问题:避坑指南
让我们开始这段精彩的STM32 HAL库学习之旅吧!
第一章:STM32 HAL库基础架构深度解析
1.1 HAL库的整体架构
STM32 HAL库采用分层设计,从上到下分为:
应用层(Application Layer)
↓
HAL库层(HAL Layer)
↓
外设驱动层(Peripheral Driver Layer)
↓
硬件抽象层(Hardware Abstraction Layer)
↓
硬件层(Hardware)
各层的作用:
- 应用层:用户编写的业务逻辑代码
- HAL库层:提供统一的API接口,封装硬件操作
- 外设驱动层:具体外设的驱动实现
- 硬件抽象层:寄存器级别的操作
- 硬件层:实际的STM32芯片
1.2 HAL库的核心文件结构
让我们看看HAL库的文件组织:
STM32Cube_FW_F1_V1.8.0/
├── Drivers/
│ ├── STM32F1xx_HAL_Driver/
│ │ ├── Inc/
│ │ │ ├── stm32f1xx_hal.h # HAL库主头文件
│ │ │ ├── stm32f1xx_hal_conf.h # HAL库配置文件
│ │ │ ├── stm32f1xx_hal_gpio.h # GPIO驱动
│ │ │ ├── stm32f1xx_hal_uart.h # UART驱动
│ │ │ ├── stm32f1xx_hal_i2c.h # I2C驱动
│ │ │ ├── stm32f1xx_hal_spi.h # SPI驱动
│ │ │ └── ... # 其他外设驱动
│ │ └── Src/
│ │ ├── stm32f1xx_hal.c # HAL库初始化
│ │ ├── stm32f1xx_hal_gpio.c # GPIO实现
│ │ ├── stm32f1xx_hal_uart.c # UART实现
│ │ └── ... # 其他外设实现
│ └── CMSIS/ # CMSIS标准接口
└── Projects/ # 示例项目
1.3 HAL库的初始化流程
每个HAL库项目都需要遵循标准的初始化流程:
c
int main(void)
{
// 1. HAL库初始化
HAL_Init();
// 2. 系统时钟配置
SystemClock_Config();
// 3. 外设初始化
MX_GPIO_Init();
MX_USART1_UART_Init();
// ... 其他外设初始化
// 4. 应用主循环
while (1)
{
// 用户代码
}
}
详细解析:
1. HAL_Init()函数
这个函数做了哪些事情?
c
HAL_StatusTypeDef HAL_Init(void)
{
// 配置Flash预取指和等待周期
__HAL_FLASH_PREFETCH_BUFFER_ENABLE();
__HAL_FLASH_SET_LATENCY(FLASH_LATENCY_2);
// 配置SysTick定时器(用于HAL_Delay)
HAL_InitTick(TICK_INT_PRIORITY);
// 配置低功耗模式
HAL_MspInit();
return HAL_OK;
}
关键点:
- Flash预取指:提高代码执行效率
- 等待周期:根据系统时钟配置
- SysTick配置 :用于
HAL_Delay()函数 - MSP初始化:用户可在此添加自定义初始化代码
2. SystemClock_Config()函数
系统时钟配置是STM32开发的第一步,也是最关键的一步:
c
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
// 配置HSE(外部高速时钟)
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 8MHz * 9 = 72MHz
HAL_RCC_OscConfig(&RCC_OscInitStruct);
// 配置系统时钟
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB = 72MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // APB1 = 36MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2 = 72MHz
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
}
时钟树解析:
外部晶振(8MHz)
↓
HSE
↓
PLL (×9)
↓
系统时钟(72MHz)
├── AHB总线(72MHz)
│ ├── CPU时钟(72MHz)
│ ├── GPIO时钟(72MHz)
│ └── DMA时钟(72MHz)
├── APB1总线(36MHz)
│ ├── USART2/3时钟(36MHz)
│ ├── I2C1/2时钟(36MHz)
│ └── TIM2/3/4时钟(72MHz, 因为APB1有×2)
└── APB2总线(72MHz)
├── USART1时钟(72MHz)
├── SPI1时钟(72MHz)
└── ADC1时钟(72MHz)
为什么需要这样配置?
- 性能优化:不同外设需要不同的时钟频率
- 功耗管理:可以动态调整时钟,降低功耗
- 稳定性:合理的时钟配置保证系统稳定运行
1.4 HAL库的错误处理机制
HAL库提供了完善的错误处理机制:
c
typedef enum
{
HAL_OK = 0x00U, // 操作成功
HAL_ERROR = 0x01U, // 一般错误
HAL_BUSY = 0x02U, // 外设忙
HAL_TIMEOUT = 0x03U // 超时
} HAL_StatusTypeDef;
使用示例:
c
HAL_StatusTypeDef status;
// 发送数据
status = HAL_UART_Transmit(&huart1, data, size, timeout);
if (status != HAL_OK)
{
// 错误处理
Error_Handler();
}
错误回调函数:
HAL库提供了错误回调机制,可以在错误发生时自动调用:
c
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// USART1错误处理
// 可以在这里重新初始化或记录错误
}
}
1.5 HAL库的配置选项
在stm32f1xx_hal_conf.h文件中,可以配置HAL库的各种选项:
c
// 使能/禁用外设模块
#define HAL_MODULE_ENABLED
#define HAL_GPIO_MODULE_ENABLED
#define HAL_UART_MODULE_ENABLED
#define HAL_I2C_MODULE_ENABLED
// ... 其他模块
// 配置SysTick
#define TICK_INT_PRIORITY 0U
// 配置断言
#define USE_FULL_ASSERT 1U
优化建议:
- 只使能需要的外设模块,减少代码体积
- 合理配置中断优先级
- 在调试时启用断言,发布时禁用
第二章:时钟系统:嵌入式系统的脉搏
2.1 时钟系统的重要性
时钟系统是STM32的"心脏",它决定了:
- 系统运行速度:CPU执行指令的速度
- 外设工作频率:各个外设的时钟频率
- 功耗水平:时钟频率直接影响功耗
- 系统稳定性:合理的时钟配置保证稳定运行
2.2 STM32F103的时钟源
STM32F103有多个时钟源:
时钟源选择
├── HSI (内部高速时钟) - 8MHz,精度±1%
├── HSE (外部高速时钟) - 4-16MHz,精度高
├── LSI (内部低速时钟) - 40kHz,用于看门狗
└── LSE (外部低速时钟) - 32.768kHz,用于RTC
各时钟源的特点:
| 时钟源 | 频率 | 精度 | 用途 | 功耗 |
|---|---|---|---|---|
| HSI | 8MHz | ±1% | 系统时钟(备用) | 低 |
| HSE | 4-16MHz | 高 | 系统时钟(推荐) | 中 |
| LSI | 40kHz | 低 | 看门狗 | 极低 |
| LSE | 32.768kHz | 高 | RTC | 极低 |
2.3 PLL锁相环:频率倍增器
PLL(Phase Locked Loop)可以将输入时钟倍频,生成更高的系统时钟:
输入时钟 → PLL → 输出时钟
8MHz → ×9 → 72MHz
PLL配置示例:
c
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
// 配置HSE为8MHz外部晶振
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
// 配置PLL
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 8MHz × 9 = 72MHz
// 注意:PLL输出频率不能超过72MHz(STM32F103)
HAL_RCC_OscConfig(&RCC_OscInitStruct);
}
PLL倍频系数表(STM32F103):
| PLLMUL值 | 倍频系数 | 8MHz输入输出 | 最大频率 |
|---|---|---|---|
| RCC_PLL_MUL_2 | ×2 | 16MHz | 72MHz |
| RCC_PLL_MUL_3 | ×3 | 24MHz | 72MHz |
| ... | ... | ... | ... |
| RCC_PLL_MUL_9 | ×9 | 72MHz | 72MHz |
| RCC_PLL_MUL_10 | ×10 | 80MHz | ❌ 超频 |
2.4 时钟树:理解时钟分配
STM32的时钟树非常复杂,让我们简化理解:
HSE(8MHz)
↓
PLL(×9)
↓
系统时钟(72MHz)
↓
┌──────────────┼──────────────┐
↓ ↓ ↓
AHB总线 APB1总线 APB2总线
(72MHz) (36MHz) (72MHz)
│ │ │
├─ CPU ├─ USART2 ├─ USART1
├─ GPIO ├─ I2C1 ├─ SPI1
├─ DMA ├─ TIM2 ├─ ADC1
└─ ... └─ ... └─ ...
重要规则:
- APB1时钟限制:最大36MHz
- APB2时钟限制:最大72MHz
- 定时器时钟:如果APB分频系数>1,定时器时钟=APB时钟×2
2.5 动态时钟配置实战
在实际项目中,我们可能需要动态调整时钟:
c
// 降低系统时钟以节省功耗
void SystemClock_Reduce(void)
{
// 切换到HSI(8MHz)
__HAL_RCC_PLL_DISABLE();
__HAL_RCC_SYSCLK_CONFIG(RCC_SYSCLKSOURCE_HSI);
// 等待切换完成
while(__HAL_RCC_GET_SYSCLK_SOURCE() != RCC_SYSCLKSOURCE_HSI);
}
// 恢复系统时钟
void SystemClock_Restore(void)
{
// 重新配置PLL
SystemClock_Config();
}
2.6 时钟配置常见问题
问题1:系统无法启动
原因:
- HSE晶振未连接或损坏
- PLL配置错误导致超频
- Flash等待周期配置错误
解决:
c
// 检查HSE是否就绪
if (__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY))
{
// HSE正常
}
else
{
// HSE异常,切换到HSI
__HAL_RCC_SYSCLK_CONFIG(RCC_SYSCLKSOURCE_HSI);
}
问题2:外设时钟未使能
原因:
- 忘记使能外设时钟
解决:
c
// 使能USART1时钟
__HAL_RCC_USART1_CLK_ENABLE();
// 使能GPIOA时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
问题3:定时器频率不对
原因:
- APB分频导致定时器时钟×2
解决:
c
// 计算实际定时器时钟
uint32_t GetTimerClock(TIM_TypeDef *TIMx)
{
uint32_t pclk;
if (TIMx == TIM1 || TIMx == TIM8 || TIMx == TIM9 ||
TIMx == TIM10 || TIMx == TIM11)
{
// APB2定时器
pclk = HAL_RCC_GetPCLK2Freq();
}
else
{
// APB1定时器
pclk = HAL_RCC_GetPCLK1Freq();
}
// 如果APB分频>1,定时器时钟×2
if ((RCC->CFGR & RCC_CFGR_PPRE1) >> 8 > 1)
{
pclk *= 2;
}
return pclk;
}
第三章:GPIO编程:点亮你的第一个LED
3.1 GPIO基础概念
GPIO(General Purpose Input/Output)是STM32最基础也是最重要的外设。每个GPIO引脚都可以配置为:
- 输入模式:读取外部信号
- 输出模式:驱动外部设备
- 复用功能:作为外设的引脚(如UART、SPI等)
- 模拟功能:作为ADC输入
3.2 GPIO工作模式详解
STM32F103的GPIO有8种工作模式:
c
typedef enum
{
GPIO_MODE_INPUT = 0x00000000U, // 输入模式
GPIO_MODE_OUTPUT_PP = 0x00000001U, // 推挽输出
GPIO_MODE_OUTPUT_OD = 0x00000002U, // 开漏输出
GPIO_MODE_AF_PP = 0x00000008U, // 复用推挽
GPIO_MODE_AF_OD = 0x00000010U, // 复用开漏
GPIO_MODE_ANALOG = 0x00000003U, // 模拟模式
GPIO_MODE_IT_RISING = 0x10110000U, // 上升沿中断
GPIO_MODE_IT_FALLING = 0x10210000U, // 下降沿中断
GPIO_MODE_IT_RISING_FALLING = 0x10310000U // 双边沿中断
} GPIO_ModeTypeDef;
各模式的应用场景:
| 模式 | 特点 | 应用场景 |
|---|---|---|
| 输入浮空 | 高阻态,易受干扰 | 不推荐使用 |
| 输入上拉 | 内部上拉电阻 | 按键输入 |
| 输入下拉 | 内部下拉电阻 | 按键输入 |
| 模拟输入 | 高阻态 | ADC采样 |
| 推挽输出 | 强驱动能力 | LED、蜂鸣器 |
| 开漏输出 | 需要外部上拉 | I2C总线 |
| 复用推挽 | 外设控制 | UART、SPI |
| 复用开漏 | 外设控制 | I2C |
3.3 GPIO配置实战:LED闪烁
让我们通过一个完整的LED闪烁项目来学习GPIO:
硬件连接:
LED正极 → 限流电阻(220Ω) → STM32 PA5
LED负极 → GND
代码实现:
c
// main.h
#include "stm32f1xx_hal.h"
// 函数声明
void SystemClock_Config(void);
void Error_Handler(void);
// main.c
int main(void)
{
// HAL库初始化
HAL_Init();
SystemClock_Config();
// GPIO初始化
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIOA时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置PA5为推挽输出
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 主循环:LED闪烁
while (1)
{
// LED亮
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
HAL_Delay(500);
// LED灭
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
HAL_Delay(500);
}
}
代码解析:
- 时钟使能 :
__HAL_RCC_GPIOA_CLK_ENABLE()- 必须使能GPIO时钟才能使用 - 引脚选择 :
GPIO_PIN_5- 选择PA5引脚 - 模式配置 :
GPIO_MODE_OUTPUT_PP- 推挽输出模式 - 速度配置 :
GPIO_SPEED_FREQ_LOW- 低速(LED不需要高速)
3.4 GPIO高级应用:按键输入
按键输入是GPIO的另一个重要应用:
硬件连接:
按键一端 → STM32 PA0
按键另一端 → GND
PA0内部上拉 → 3.3V
代码实现:
c
// 按键初始化
void Key_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIOA时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置PA0为输入上拉模式
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉,按键按下为低电平
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
// 按键扫描(带消抖)
uint8_t Key_Scan(void)
{
static uint8_t key_state = 1; // 按键状态
static uint32_t key_time = 0; // 按键时间
uint8_t key_press = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
if (key_press == 0 && key_state == 1) // 检测到按下
{
if (HAL_GetTick() - key_time > 20) // 消抖20ms
{
key_state = 0;
key_time = HAL_GetTick();
return 1; // 返回按键按下
}
}
else if (key_press == 1 && key_state == 0) // 检测到释放
{
key_state = 1;
key_time = HAL_GetTick();
}
return 0;
}
// 主循环中使用
int main(void)
{
HAL_Init();
SystemClock_Config();
Key_Init();
while (1)
{
if (Key_Scan())
{
// 按键按下,执行操作
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 切换LED状态
}
}
}
按键消抖原理:
机械按键在按下和释放时会产生抖动,需要软件消抖:
时间轴 →
按键状态: 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 1 1
↑抖动↑ ↑稳定按下↑ ↑抖动↑
消抖后: 1 0 1
3.5 GPIO位操作:高效控制
HAL库提供了位操作函数,可以高效地控制单个引脚:
c
// 设置引脚为高电平
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
// 设置引脚为低电平
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
// 读取引脚状态
GPIO_PinState state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5);
// 翻转引脚状态
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
// 锁定引脚配置(防止意外修改)
HAL_GPIO_LockPin(GPIOA, GPIO_PIN_5);
性能对比:
| 方法 | 执行时间 | 代码大小 |
|---|---|---|
| HAL库函数 | ~10个时钟周期 | 较大 |
| 寄存器操作 | ~2个时钟周期 | 小 |
| 位带操作 | ~1个时钟周期 | 小 |
寄存器直接操作(高级用法):
c
// 直接操作寄存器,速度最快
GPIOA->BSRR = GPIO_PIN_5; // 置位
GPIOA->BRR = GPIO_PIN_5; // 复位
GPIOA->ODR ^= GPIO_PIN_5; // 翻转
uint8_t state = (GPIOA->IDR >> 5) & 0x01; // 读取
3.6 GPIO复用功能:外设引脚配置
当GPIO用作外设功能时,需要配置为复用模式:
c
// 配置PA9为USART1_TX(复用推挽输出)
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速(串口需要)
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
STM32F103引脚复用表(部分):
| 引脚 | 功能1 | 功能2 | 功能3 |
|---|---|---|---|
| PA9 | GPIO | USART1_TX | TIM1_CH2 |
| PA10 | GPIO | USART1_RX | TIM1_CH3 |
| PB6 | GPIO | I2C1_SCL | TIM4_CH1 |
| PB7 | GPIO | I2C1_SDA | TIM4_CH2 |
| PB10 | GPIO | USART3_TX | I2C2_SCL |
| PB11 | GPIO | USART3_RX | I2C2_SDA |
3.7 GPIO实战项目:流水灯
让我们实现一个经典的流水灯项目:
c
// 定义LED引脚
#define LED1_PIN GPIO_PIN_5
#define LED2_PIN GPIO_PIN_6
#define LED3_PIN GPIO_PIN_7
#define LED_PORT GPIOA
// LED初始化
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = LED1_PIN | LED2_PIN | LED3_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(LED_PORT, &GPIO_InitStruct);
// 初始状态:全部熄灭
HAL_GPIO_WritePin(LED_PORT, LED1_PIN | LED2_PIN | LED3_PIN, GPIO_PIN_RESET);
}
// 流水灯效果
void LED_Flow(void)
{
// LED1亮
HAL_GPIO_WritePin(LED_PORT, LED1_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(LED_PORT, LED2_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED_PORT, LED3_PIN, GPIO_PIN_RESET);
HAL_Delay(200);
// LED2亮
HAL_GPIO_WritePin(LED_PORT, LED1_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED_PORT, LED2_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(LED_PORT, LED3_PIN, GPIO_PIN_RESET);
HAL_Delay(200);
// LED3亮
HAL_GPIO_WritePin(LED_PORT, LED1_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED_PORT, LED2_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED_PORT, LED3_PIN, GPIO_PIN_SET);
HAL_Delay(200);
}
int main(void)
{
HAL_Init();
SystemClock_Config();
LED_Init();
while (1)
{
LED_Flow();
}
}
第四章:中断系统:让系统更智能
4.1 中断系统概述
中断是嵌入式系统的核心机制之一,它允许CPU在执行主程序时,响应外部事件或内部异常。
中断的优势:
- 实时响应:立即响应外部事件
- 提高效率:避免轮询,节省CPU资源
- 多任务处理:看似同时处理多个任务
4.2 STM32中断优先级
STM32使用NVIC(Nested Vectored Interrupt Controller)管理中断:
优先级分组:
c
// 优先级分组配置(在HAL_Init中)
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
// 4位优先级分组:
// - 抢占优先级:0-15(高优先级可以打断低优先级)
// - 子优先级:0-15(相同抢占优先级时,子优先级高的先执行)
优先级配置示例:
c
// 配置USART1中断优先级
HAL_NVIC_SetPriority(USART1_IRQn, 2, 0); // 抢占优先级2,子优先级0
HAL_NVIC_EnableIRQ(USART1_IRQn); // 使能中断
4.3 GPIO外部中断实战
GPIO外部中断是最常用的中断类型:
硬件连接:
按键 → PA0(外部中断)
代码实现:
c
// GPIO外部中断初始化
void EXTI_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIOA时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置PA0为输入上拉,下降沿触发中断
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿中断
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置中断优先级
HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
// 外部中断回调函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
// 按键按下处理
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 切换LED
}
}
// 中断服务函数(在stm32f1xx_it.c中)
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
中断触发模式:
c
// 上升沿触发
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
// 下降沿触发
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
// 双边沿触发
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING_FALLING;
4.4 串口中断:高效的数据接收
串口中断可以高效地接收数据,避免丢失:
c
// 串口中断接收初始化
UART_HandleTypeDef huart1;
uint8_t uart_rx_buffer[256];
uint8_t uart_rx_index = 0;
void UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart1);
// 启动接收中断
HAL_UART_Receive_IT(&huart1, &uart_rx_buffer[uart_rx_index], 1);
// 使能中断
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
// 串口接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// 处理接收到的数据
if (uart_rx_buffer[uart_rx_index] == '\n')
{
// 收到完整的一行数据
uart_rx_buffer[uart_rx_index] = '\0';
ProcessCommand((char*)uart_rx_buffer);
uart_rx_index = 0;
}
else
{
uart_rx_index++;
if (uart_rx_index >= 255) uart_rx_index = 0;
}
// 继续接收下一个字节
HAL_UART_Receive_IT(&huart1, &uart_rx_buffer[uart_rx_index], 1);
}
}
4.5 中断嵌套与优先级管理
中断嵌套规则:
主程序(优先级最低)
↓
中断A(抢占优先级2)
↓
中断B(抢占优先级1)← 可以打断中断A
↓
中断C(抢占优先级0)← 可以打断中断B
最佳实践:
- 关键中断设置高优先级:如看门狗、系统错误
- 通信中断设置中优先级:如UART、SPI
- 用户中断设置低优先级:如按键、LED
c
// 系统关键中断(最高优先级)
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
// 通信中断(中等优先级)
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0);
HAL_NVIC_SetPriority(SPI1_IRQn, 1, 1);
// 用户中断(低优先级)
HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0);
第五章:定时器应用:精准的时间控制
5.1 定时器概述
STM32F103有多种定时器:
- 高级定时器:TIM1、TIM8(带死区控制,用于电机控制)
- 通用定时器:TIM2-TIM5(最常用)
- 基本定时器:TIM6、TIM7(用于DAC触发)
- SysTick:系统滴答定时器(用于HAL_Delay)
5.2 定时器工作原理
定时器的核心是计数器:
时钟源 → 预分频器 → 计数器 → 比较/捕获
72MHz → /7200 → 0-9999 → 触发中断/输出PWM
定时器计算公式:
定时时间 = (ARR + 1) × (PSC + 1) / 定时器时钟频率
例如:
定时器时钟 = 72MHz
PSC = 7199 (分频7200)
ARR = 9999 (计数值10000)
定时时间 = 10000 × 7200 / 72000000 = 1秒
5.3 定时器中断:精准延时
使用定时器中断实现精准的定时功能:
c
TIM_HandleTypeDef htim2;
// 定时器初始化(1ms中断)
void TIM2_Init(void)
{
htim2.Instance = TIM2;
htim2.Init.Prescaler = 7200 - 1; // 72MHz / 7200 = 10kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 10 - 1; // 10kHz / 10 = 1kHz (1ms)
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
// 使能定时器中断
HAL_TIM_Base_Start_IT(&htim2);
// 配置中断优先级
HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
// 定时器中断回调
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2)
{
// 1ms定时任务
static uint32_t counter = 0;
counter++;
if (counter >= 1000) // 1秒
{
counter = 0;
// 执行1秒定时任务
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
}
}
5.4 PWM输出:控制电机和LED亮度
PWM(Pulse Width Modulation)是定时器的重要应用:
c
TIM_HandleTypeDef htim3;
TIM_OC_InitTypeDef sConfigOC = {0};
// PWM初始化(通道1,频率1kHz)
void PWM_Init(void)
{
// 定时器基础配置
htim3.Instance = TIM3;
htim3.Init.Prescaler = 72 - 1; // 72MHz / 72 = 1MHz
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 1000 - 1; // 1MHz / 1000 = 1kHz
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&htim3);
// PWM通道配置
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500; // 占空比50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
// 启动PWM输出
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}
// 改变PWM占空比
void PWM_SetDuty(uint16_t duty) // duty: 0-1000
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty);
}
// LED呼吸灯效果
void LED_Breathing(void)
{
static uint16_t duty = 0;
static int8_t direction = 1;
duty += direction * 10;
if (duty >= 1000)
{
duty = 1000;
direction = -1;
}
else if (duty <= 0)
{
duty = 0;
direction = 1;
}
PWM_SetDuty(duty);
HAL_Delay(10);
}
PWM波形:
占空比50%:
┌─────┐ ┌─────┐
│ │ │ │
────┘ └─────┘ └─────
占空比25%:
┌──┐ ┌──┐
│ │ │ │
────┘ └─────────┘ └─────
5.5 输入捕获:测量频率和脉宽
输入捕获可以测量外部信号的频率和脉宽:
c
TIM_HandleTypeDef htim4;
uint32_t capture_value1 = 0;
uint32_t capture_value2 = 0;
uint32_t frequency = 0;
// 输入捕获初始化
void InputCapture_Init(void)
{
htim4.Instance = TIM4;
htim4.Init.Prescaler = 72 - 1; // 72MHz / 72 = 1MHz
htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
htim4.Init.Period = 0xFFFF; // 16位最大值
htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_IC_Init(&htim4);
// 配置输入捕获通道
TIM_IC_InitTypeDef sConfigIC;
sConfigIC.ICFilter = 0;
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
HAL_TIM_IC_ConfigChannel(&htim4, &sConfigIC, TIM_CHANNEL_1);
// 启动输入捕获
HAL_TIM_IC_Start_IT(&htim4, TIM_CHANNEL_1);
HAL_NVIC_SetPriority(TIM4_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(TIM4_IRQn);
}
// 输入捕获回调
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM4)
{
if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
capture_value2 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
if (capture_value2 > capture_value1)
{
// 计算频率(单位:Hz)
frequency = 1000000 / (capture_value2 - capture_value1);
}
capture_value1 = capture_value2;
}
}
}
第六章:串口通信:设备间的桥梁
6.1 串口通信基础
UART(Universal Asynchronous Receiver/Transmitter)是最常用的串行通信接口。
串口通信参数:
- 波特率:数据传输速度(如115200bps)
- 数据位:5-9位(通常8位)
- 停止位:1或2位
- 校验位:无、奇、偶
6.2 串口初始化与配置
c
UART_HandleTypeDef huart1;
// 串口初始化
void UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart1);
}
// 串口MSP初始化(配置GPIO)
void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(huart->Instance == USART1)
{
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// PA9: USART1_TX
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA10: USART1_RX
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
}
6.3 串口发送数据
阻塞方式发送:
c
// 发送字符串
void UART_SendString(char* str)
{
HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), HAL_MAX_DELAY);
}
// 发送格式化字符串
void UART_Printf(const char* format, ...)
{
char buffer[256];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
HAL_UART_Transmit(&huart1, (uint8_t*)buffer, strlen(buffer), HAL_MAX_DELAY);
}
非阻塞方式发送:
c
// 启动发送
HAL_UART_Transmit_IT(&huart1, data, size);
// 发送完成回调
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// 发送完成,可以继续发送下一批数据
}
}
6.4 串口接收数据
阻塞方式接收:
c
uint8_t rx_buffer[256];
HAL_UART_Receive(&huart1, rx_buffer, 256, 5000); // 5秒超时
中断方式接收(推荐):
c
uint8_t uart_rx_buffer[256];
uint16_t uart_rx_index = 0;
// 启动接收
HAL_UART_Receive_IT(&huart1, &uart_rx_buffer[uart_rx_index], 1);
// 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
if (uart_rx_buffer[uart_rx_index] == '\n')
{
// 收到完整的一行
uart_rx_buffer[uart_rx_index] = '\0';
ProcessCommand((char*)uart_rx_buffer);
uart_rx_index = 0;
}
else
{
uart_rx_index++;
}
// 继续接收
HAL_UART_Receive_IT(&huart1, &uart_rx_buffer[uart_rx_index], 1);
}
}
6.5 串口通信实战:命令解析器
实现一个简单的命令解析器:
c
typedef struct {
char* cmd;
void (*handler)(char* args);
} Command_t;
// 命令表
Command_t commands[] = {
{"LED_ON", CMD_LED_On},
{"LED_OFF", CMD_LED_Off},
{"GET_TEMP", CMD_GetTemp},
{NULL, NULL}
};
// 命令处理
void ProcessCommand(char* cmd_line)
{
char cmd[32];
char args[64];
// 解析命令和参数
sscanf(cmd_line, "%s %s", cmd, args);
// 查找命令
for (int i = 0; commands[i].cmd != NULL; i++)
{
if (strcmp(cmd, commands[i].cmd) == 0)
{
commands[i].handler(args);
return;
}
}
// 未找到命令
UART_SendString("Unknown command\r\n");
}
// 命令处理函数
void CMD_LED_On(char* args)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
UART_SendString("LED ON\r\n");
}
void CMD_LED_Off(char* args)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
UART_SendString("LED OFF\r\n");
}
第七章:I2C总线:多设备协同的纽带
7.1 I2C总线基础
I2C(Inter-Integrated Circuit)是一种两线制串行总线:
- SDA:数据线
- SCL:时钟线
I2C特点:
- 多主多从架构
- 7位或10位地址
- 标准模式100kHz,快速模式400kHz
- 需要上拉电阻(通常4.7kΩ)
7.2 I2C初始化配置
c
I2C_HandleTypeDef hi2c1;
// I2C初始化
void I2C_Init(void)
{
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000; // 100kHz
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
HAL_I2C_Init(&hi2c1);
}
// I2C MSP初始化
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(hi2c->Instance == I2C1)
{
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_I2C1_CLK_ENABLE();
// PB6: I2C1_SCL
// PB7: I2C1_SDA
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
}
7.3 I2C读写操作
写入数据:
c
// 向从设备写入数据
uint8_t data[2] = {0x00, 0x55};
HAL_I2C_Master_Transmit(&hi2c1, 0xA0, data, 2, 1000);
读取数据:
c
// 从从设备读取数据
uint8_t data[4];
HAL_I2C_Master_Receive(&hi2c1, 0xA0, data, 4, 1000);
写入后读取(常用):
c
// 先写入寄存器地址,再读取数据
uint8_t reg_addr = 0x00;
uint8_t data;
HAL_I2C_Mem_Read(&hi2c1, 0xA0, reg_addr, I2C_MEMADD_SIZE_8BIT, &data, 1, 1000);
7.4 I2C实战:OLED显示屏驱动
以SSD1306 OLED显示屏为例:
c
#define OLED_ADDRESS 0x3C
// OLED初始化
void OLED_Init(void)
{
// 初始化序列
uint8_t init_cmd[] = {
0xAE, // 关闭显示
0xD5, 0x80, // 设置时钟分频
0xA8, 0x3F, // 设置多路复用
0xD3, 0x00, // 设置显示偏移
0x40, // 设置起始行
0x8D, 0x14, // 电荷泵使能
0x20, 0x00, // 内存地址模式
0xA1, // 段重映射
0xC8, // COM扫描方向
0xDA, 0x12, // COM引脚配置
0x81, 0xCF, // 对比度设置
0xD9, 0xF1, // 预充电周期
0xDB, 0x40, // VCOMH设置
0xA4, // 全部显示开启
0xA6, // 正常显示
0xAF // 开启显示
};
// 发送初始化命令
for (int i = 0; i < sizeof(init_cmd); i++)
{
OLED_WriteCommand(init_cmd[i]);
}
}
// 写入命令
void OLED_WriteCommand(uint8_t cmd)
{
uint8_t data[2] = {0x00, cmd}; // 0x00表示命令
HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDRESS, data, 2, 1000);
}
// 写入数据
void OLED_WriteData(uint8_t data)
{
uint8_t buf[2] = {0x40, data}; // 0x40表示数据
HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDRESS, buf, 2, 1000);
}
第八章:SPI通信:高速数据传输
8.1 SPI总线基础
SPI(Serial Peripheral Interface)是一种高速全双工串行总线:
- MOSI:主出从入
- MISO:主入从出
- SCK:时钟
- CS/NSS:片选
SPI特点:
- 全双工通信
- 高速(可达几十MHz)
- 主从架构
- 需要片选信号
8.2 SPI初始化配置
c
SPI_HandleTypeDef hspi1;
// SPI初始化
void SPI_Init(void)
{
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 9MHz
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
HAL_SPI_Init(&hspi1);
}
8.3 SPI读写操作
c
// SPI发送数据
uint8_t tx_data = 0x55;
uint8_t rx_data;
HAL_SPI_TransmitReceive(&hspi1, &tx_data, &rx_data, 1, 1000);
// SPI只发送
HAL_SPI_Transmit(&hspi1, tx_buffer, size, 1000);
// SPI只接收
HAL_SPI_Receive(&hspi1, rx_buffer, size, 1000);
第九章:ADC采集:感知世界的窗口
9.1 ADC基础
ADC(Analog-to-Digital Converter)将模拟信号转换为数字信号。
STM32F103 ADC特性:
- 12位分辨率(0-4095)
- 多个通道(最多18个)
- 单次/连续转换模式
- 扫描模式
9.2 ADC初始化配置
c
ADC_HandleTypeDef hadc1;
// ADC初始化
void ADC_Init(void)
{
hadc1.Instance = ADC1;
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
HAL_ADC_Init(&hadc1);
// 配置通道
ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}
// ADC读取
uint16_t ADC_Read(void)
{
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 1000);
uint16_t value = HAL_ADC_GetValue(&hadc1);
HAL_ADC_Stop(&hadc1);
return value;
}
// 转换为电压(参考电压3.3V)
float ADC_ToVoltage(uint16_t adc_value)
{
return (float)adc_value * 3.3f / 4095.0f;
}
第十章:DMA传输:解放CPU的利器
10.1 DMA基础
DMA(Direct Memory Access)可以在不占用CPU的情况下传输数据。
DMA优势:
- 提高系统效率
- 减少CPU占用
- 支持大数据传输
10.2 DMA配置示例
c
DMA_HandleTypeDef hdma_usart1_tx;
// DMA初始化(用于UART发送)
void DMA_Init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_usart1_tx.Instance = DMA1_Channel4;
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_tx.Init.Mode = DMA_NORMAL;
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
HAL_DMA_Init(&hdma_usart1_tx);
__HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx);
}
// DMA发送数据
void UART_DMA_Send(uint8_t* data, uint16_t size)
{
HAL_UART_Transmit_DMA(&huart1, data, size);
}
第十一章:低功耗设计:让设备更持久
11.1 STM32低功耗模式
STM32有多种低功耗模式:
- 睡眠模式:CPU停止,外设运行
- 停止模式:大部分模块停止
- 待机模式:最低功耗
11.2 进入低功耗模式
c
// 进入停止模式
void Enter_StopMode(void)
{
// 配置唤醒源
HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1);
// 进入停止模式
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// 唤醒后重新配置时钟
SystemClock_Config();
}
第十二章:综合实战项目:智能环境监测系统
12.1 项目需求
设计一个智能环境监测系统,功能包括:
- 温湿度采集(DHT22传感器)
- 光照强度采集(光敏电阻+ADC)
- OLED显示数据
- 串口输出数据
- 按键控制
12.2 系统架构
STM32F103C8T6
├── DHT22传感器(单总线)
├── 光敏电阻(ADC采集)
├── OLED显示屏(I2C)
├── 按键(GPIO中断)
└── 串口(调试输出)
12.3 代码实现框架
c
int main(void)
{
HAL_Init();
SystemClock_Config();
// 外设初始化
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_I2C1_Init();
MX_ADC1_Init();
MX_TIM2_Init();
// 传感器初始化
DHT22_Init();
OLED_Init();
// 主循环
while (1)
{
// 读取传感器数据
float temp = DHT22_ReadTemperature();
float humi = DHT22_ReadHumidity();
uint16_t light = ADC_ReadLight();
// 显示数据
OLED_DisplayData(temp, humi, light);
// 串口输出
UART_Printf("Temp: %.1fC, Humi: %.1f%%, Light: %d\r\n",
temp, humi, light);
// 延时
HAL_Delay(1000);
}
}
总结与展望
通过本文档的学习,你应该已经掌握了:
- ✅ HAL库的基础架构和使用方法
- ✅ 时钟系统的配置和优化
- ✅ GPIO的配置和应用
- ✅ 中断系统的使用
- ✅ 定时器的各种应用
- ✅ 串口通信的实现
- ✅ I2C和SPI总线通信
- ✅ ADC数据采集
- ✅ DMA高效传输
- ✅ 低功耗设计
- ✅ 综合项目开发
下一步学习建议:
- 深入学习:研究HAL库源码,理解底层实现
- 项目实践:完成更多实际项目,积累经验
- 性能优化:学习代码优化技巧,提高效率
- RTOS应用:学习FreeRTOS等实时操作系统
- 高级外设:学习USB、以太网等高级外设
祝您学习顺利,开发愉快!