本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:STM32是意法半导体推出的基于ARM Cortex-M内核的高性能、低功耗微控制器系列,广泛应用于物联网、电机控制和人机交互等领域。本教程专为初学者设计,涵盖STM32F103核心知识,从开发环境搭建、GPIO操作、中断定时器、串口通信到ADC/DAC、CAN总线、USB接口等外设使用,并引入RTOS和实战项目,帮助学习者掌握嵌入式系统开发全流程。
STM32.zip_STM32单片机_STM32教程_stm32 tutorial_stm32优秀教材_stm单片机教程

1. STM32单片机架构与内核基础

STM32系列单片机基于ARM Cortex-M3内核构建,具备高性能、低成本和低功耗等优势。Cortex-M3内核采用三级流水线结构,支持Thumb-2指令集,具备高效的中断处理能力,通过嵌套向量中断控制器(NVIC)实现快速中断响应。存储器架构采用哈佛结构,程序与数据存储分离,提升执行效率。系统时钟由多个时钟源(如HSI、HSE、PLL)组成,通过时钟树灵活配置,为CPU和外设提供稳定时钟源。本章将从底层硬件角度解析STM32的运行机制,帮助开发者构建扎实的系统认知基础。

2. STM32开发环境搭建与工程配置

在嵌入式系统开发中,开发环境的搭建是整个项目开发流程的第一步,也是最为关键的环节之一。一个稳定、高效的开发环境不仅能提升开发效率,还能显著降低调试过程中的复杂度。对于STM32系列单片机而言,开发环境通常包括开发工具的安装、工程创建与配置、代码生成、程序烧录以及调试手段的设置等多个方面。本章将从开发工具的选择与安装入手,详细讲解如何搭建一个完整的STM32开发环境,并通过具体的操作步骤与配置方法,帮助读者掌握工程创建与配置的核心流程。

2.1 开发工具的选择与安装

在STM32开发中,常见的开发工具主要包括Keil MDK(Microcontroller Development Kit)和STM32CubeIDE。它们各自具备不同的优势和适用场景,选择合适的开发工具对于开发效率和项目管理具有重要意义。

2.1.1 Keil MDK与STM32CubeIDE的对比分析

特性 Keil MDK STM32CubeIDE
开发厂商 Arm(现为Keil公司) STMicroelectronics
支持架构 支持Cortex-M系列多种芯片 专为STM32系列优化
图形化配置工具 不内置,需配合STM32CubeMX使用 集成STM32CubeMX,支持图形化配置
编译器 ARMCC(Keil编译器) GCC(开源编译器)
调试器支持 支持J-Link、ULINK、ST-Link等 支持ST-Link,兼容J-Link
插件生态 丰富的第三方插件 ST官方插件支持丰富
免费版限制 有代码大小限制(32KB) 完全免费
用户界面 经典IDE风格,界面较传统 现代化IDE,基于Eclipse架构
实时操作系统支持 支持RTX等RTOS 支持FreeRTOS、ThreadX等

从上表可以看出,Keil MDK以其成熟的工具链和稳定的编译器性能被广泛应用于商业项目中,尤其适合对代码优化要求较高的场景。而STM32CubeIDE则更注重与STM32芯片的深度集成,提供了从芯片配置到代码生成、调试的全流程支持,适合快速开发和学习使用。

适用建议:
- 对于学习者或项目初期快速验证,推荐使用STM32CubeIDE。
- 对于产品级项目、代码优化要求高或已有Keil项目迁移,建议使用Keil MDK。

2.1.2 安装步骤与常见问题处理

Keil MDK 安装步骤
  1. 下载安装包 :访问Keil官网,下载对应版本的MDK(建议选择MDK-ARM版本)。
  2. 运行安装程序 :双击安装包,选择安装路径。
  3. 注册License :安装完成后,打开Keil uVision,进入“File” -> “License Management”,复制CID码,访问Keil官网生成License。
  4. 安装设备支持包 :根据所使用的STM32型号,下载对应的Device Family Pack(DFP)并安装。
STM32CubeIDE 安装步骤
  1. 下载安装包 :访问ST官网,下载最新版本的STM32CubeIDE。
  2. 解压并运行 :STM32CubeIDE为绿色软件,解压后直接运行 stm32cubeide.exe 即可。
  3. 安装插件(可选) :启动后可通过“Help” -> “Eclipse Marketplace”安装额外插件,如STM32CubeProgrammer等。
常见问题与解决方法
问题描述 解决方法
安装后无法识别芯片型号 确认已安装对应的DFP包或更新STM32CubeIDE
程序编译时报错“undefined reference” 检查是否正确链接了对应的外设库文件或是否启用相关外设时钟
J-Link调试器无法连接 更新J-Link驱动,检查目标板供电与连接是否正常
串口调试无输出 检查波特率配置是否与串口助手一致,确认USART是否启用
STM32CubeMX无法启动 确认Java运行环境是否安装,或尝试以管理员身份运行

2.2 工程创建与配置流程

工程的创建与配置是STM32开发的核心环节之一,直接影响后续的代码编写、调试和烧录过程。本节将介绍如何使用STM32CubeMX生成初始化代码,并配置系统时钟与外设模块。

2.2.1 使用STM32CubeMX生成初始化代码

STM32CubeMX是一款图形化配置工具,支持从芯片选型、外设配置到代码生成的全流程操作。以下是使用STM32CubeMX生成初始化代码的详细步骤:

graph TD
    A[打开STM32CubeMX] --> B[选择芯片型号]
    B --> C[配置引脚功能]
    C --> D[配置时钟系统]
    D --> E[启用外设模块]
    E --> F[生成代码]
    F --> G[导出为Keil或STM32CubeIDE工程]

操作步骤:

  1. 选择芯片型号 :点击“Start My Project”,在搜索框中输入目标芯片型号(如STM32F407VG)。
  2. 配置引脚功能 :进入“Pinout & Configuration”页面,点击引脚设置其功能,如GPIO、USART、SPI等。
  3. 配置系统时钟 :进入“Clock Configuration”页面,设置HSE、PLL等参数,生成系统时钟频率。
  4. 启用外设模块 :在“Configuration”页面中启用所需外设(如TIM、ADC、I2C等),并配置其参数。
  5. 生成代码 :点击“Project” -> “Generate Code”,选择目标IDE(如Keil、STM32CubeIDE),设置工程路径后生成代码。
  6. 导出工程 :生成完成后,打开对应IDE导入项目即可。

2.2.2 配置系统时钟与外设模块

系统时钟是STM32运行的基础,影响CPU速度、外设定时精度等关键性能。以下是基于STM32F407的系统时钟配置示例:

// 系统时钟配置函数(由STM32CubeMX生成)
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.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 8;
  RCC_OscInitStruct.PLL.PLLN = 336;
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
  RCC_OscInitStruct.PLL.PLLQ = 7;
  HAL_RCC_OscConfig(&RCC_OscInitStruct);

  // 初始化CPU、AHB、APB总线时钟
  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;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
  HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5);
}

逐行解读与参数说明:

  • RCC_OscInitTypeDef :用于配置振荡器类型和参数。
  • OscillatorType :选择使用HSE(高速外部时钟)。
  • HSEState :启用HSE。
  • PLLState :启用锁相环(PLL)。
  • PLLM/N/P/Q :PLL倍频与分频参数,最终系统时钟 = HSE / PLLM * PLLN / PLLP = 8MHz / 8 * 336 / 2 = 168MHz。
  • RCC_ClkInitTypeDef :用于配置系统时钟源与总线分频。
  • SYSCLKSource :选择PLL作为系统时钟源。
  • AHBCLKDivider :AHB总线不分频,即168MHz。
  • APB1CLKDivider :APB1总线分频为1/4,即42MHz。
  • APB2CLKDivider :APB2总线分频为1/2,即84MHz。

2.3 程序烧录与调试环境搭建

程序烧录和调试是验证代码是否正确运行的重要环节。本节将介绍常用的调试工具(如J-Link与ST-Link)的使用方法,并讲解串口打印与断点调试技巧。

2.3.1 J-Link与ST-Link调试器的使用

特性 J-Link ST-Link
制造商 SEGGER STMicroelectronics
支持芯片 多种Cortex-M系列 STM32全系列
调试接口 SWD/JTAG SWD
速度 极快(高达3MHz) 快(最高1.6MHz)
支持协议 SWO、RTT等 基本SWD调试
成本 较高(专业版) 低成本(ST-LINK/V2)
兼容性 支持Keil、IAR、GDB等 主要支持STM32CubeIDE、Keil

使用步骤(以STM32CubeIDE为例):

  1. 将ST-Link调试器连接至目标板的SWD接口。
  2. 打开STM32CubeIDE,点击“Run” -> “Debug As” -> “STM32 Cortex-M C/C++ Application”。
  3. IDE会自动编译、下载程序,并进入调试界面。
  4. 使用“Step Over”、“Step Into”、“Breakpoint”等功能进行调试。

2.3.2 串口打印与断点调试技巧

在嵌入式开发中,串口打印是调试最常用的方式之一。以下是一个使用USART1进行串口打印的代码示例:

#include "stdio.h"

// 重定向printf函数至串口
int __io_putchar(int ch) {
    HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();

    printf("STM32 Booted Successfully!\r\n");

    while (1) {
        // 主循环
    }
}

逐行说明:

  • __io_putchar :重定向标准输出 printf 函数至串口发送函数。
  • HAL_UART_Transmit :调用HAL库发送单个字符。
  • printf :在main函数中输出启动信息。

调试技巧:

  • 断点设置 :可在代码中设置断点观察变量值、寄存器状态。
  • Watch窗口 :查看变量实时值。
  • Memory查看 :检查内存地址内容。
  • SWO调试输出 :若使用J-Link,可启用SWO实现高速日志输出。
  • Trace功能 :追踪程序执行路径,分析异常跳转。

本章系统地讲解了STM32开发环境的搭建与工程配置流程,从开发工具的选择、安装到工程创建、系统时钟配置,再到程序烧录与调试手段的设置,内容由浅入深,逻辑清晰,涵盖了STM32开发的全流程基础操作。下一章将进入GPIO配置与基础输入输出操作,进一步深入实践层面。

3. GPIO配置与基础输入输出操作

在嵌入式系统开发中,通用输入输出(GPIO)是最基础也是最常用的外设模块之一。STM32系列单片机提供了高度灵活的GPIO配置选项,支持多种输入输出模式、上下拉电阻、推挽/开漏结构等,适用于LED控制、按键检测、外设通信等应用场景。本章将深入解析GPIO的硬件结构、配置方法及基础输入输出操作的实现原理,并结合实际代码演示其应用方式。

3.1 GPIO引脚功能与工作模式

STM32的每个GPIO引脚都可以被配置为多种工作模式,包括输入、输出、复用功能和模拟模式。这些模式通过寄存器设置进行控制,为开发者提供了极大的灵活性。

3.1.1 输入/输出模式详解

GPIO引脚可以配置为以下几种输入输出模式:

模式类型 功能描述
输入浮空 引脚处于高阻态,需外部上拉或下拉
输入上拉 内部上拉电阻使引脚默认为高电平
输入下拉 内部下拉电阻使引脚默认为低电平
模拟输入 引脚连接到ADC或DAC,用于模拟信号采集
推挽输出 输出高低电平驱动能力强,适合LED控制
开漏输出 输出端只能拉低,需外部上拉电阻,适用于I2C等总线
复用推挽 引脚用于外设功能,如UART、SPI
复用开漏 同上,但输出结构为开漏
示例代码:配置GPIO为推挽输出模式

以下代码片段使用STM32 HAL库配置GPIO引脚为推挽输出:

GPIO_InitTypeDef GPIO_InitStruct = {0};

/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();

/* Configure PA5 as output push-pull */
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);

代码逻辑分析:

  • __HAL_RCC_GPIOA_CLK_ENABLE() :使能GPIOA时钟,是访问其寄存器的前提。
  • GPIO_InitStruct.Pin = GPIO_PIN_5 :选择PA5引脚。
  • GPIO_MODE_OUTPUT_PP :设置为推挽输出模式。
  • GPIO_NOPULL :不使用上下拉电阻。
  • GPIO_SPEED_FREQ_LOW :设置引脚翻转速度为低速,适用于LED控制。

3.1.2 上拉/下拉电阻与推挽/开漏配置

GPIO引脚内部支持上拉和下拉电阻配置,这对按键检测非常关键。例如,按键的一端接地,另一端连接到GPIO引脚,若配置为上拉模式,按键未按下时引脚为高电平,按下后为低电平。

示例代码:配置GPIO为上拉输入模式
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

参数说明:

  • GPIO_MODE_INPUT :配置为输入模式。
  • GPIO_PULLUP :启用内部上拉电阻。
  • 无需设置 Speed ,因为输入模式下速度不适用。
推挽 vs 开漏对比
特性 推挽输出 开漏输出
高电平驱动 主动驱动高电平 需外部上拉电阻
低电平驱动 主动驱动低电平 主动驱动低电平
电流驱动能力
应用场景 LED控制、数字输出 I2C总线、多设备共享总线

3.2 基础外设控制实践

本节将通过实际案例展示如何使用GPIO控制LED闪烁和检测按键输入,并介绍按键去抖的基本方法。

3.2.1 LED闪烁控制与按键输入检测

LED闪烁是最常见的GPIO输出应用。以下代码实现了一个简单的LED闪烁程序:

while (1)
{
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    HAL_Delay(500);  // 500ms delay
}

逻辑分析:

  • HAL_GPIO_TogglePin :切换GPIO引脚状态,实现LED闪烁。
  • HAL_Delay :调用系统延时函数,需确保系统时钟已正确配置。
按键输入检测

按键检测通常通过轮询方式实现:

if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
{
    // 按键按下,执行操作
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    HAL_Delay(200);  // 简单去抖延时
}

3.2.2 按键去抖与状态读取方法

机械按键在按下和释放时会产生抖动,导致多次误触发。去抖可通过硬件(RC电路)或软件(延时或状态机)实现。

软件去抖流程图(mermaid)
graph TD
    A[开始] --> B{按键是否按下?}
    B -- 否 --> A
    B -- 是 --> C[延时20ms]
    C --> D{再次检测按键是否按下?}
    D -- 否 --> A
    D -- 是 --> E[确认按键按下]
状态机实现去抖(伪代码)
typedef enum {
    BUTTON_IDLE,
    BUTTON_PRESSED,
    BUTTON_CONFIRMED
} ButtonState;

ButtonState state = BUTTON_IDLE;

void check_button(void)
{
    switch(state)
    {
        case BUTTON_IDLE:
            if (read_pin() == 0) {
                state = BUTTON_PRESSED;
                start_timer(20);
            }
            break;

        case BUTTON_PRESSED:
            if (timer_expired()) {
                if (read_pin() == 0)
                    state = BUTTON_CONFIRMED;
                else
                    state = BUTTON_IDLE;
            }
            break;

        case BUTTON_CONFIRMED:
            // 按键确认按下,执行操作
            state = BUTTON_IDLE;
            break;
    }
}

逻辑分析:

  • 使用状态机避免误判,确保按键稳定按下后再执行操作。
  • 可扩展为多按键处理,适用于复杂交互场景。

3.3 GPIO中断与事件响应

GPIO中断是一种高效的事件驱动机制,适用于需要快速响应外部输入的场景,如按键中断、传感器触发等。

3.3.1 外部中断配置与优先级设置

STM32的外部中断由EXTI(External Interrupt)控制器管理,每个GPIO引脚可映射到一个EXTI线。

配置步骤:
  1. 配置GPIO为输入模式(如上拉)。
  2. 配置EXTI线与GPIO引脚关联。
  3. 配置NVIC中断优先级并使能。
  4. 编写中断服务函数。
示例代码:配置PA0为外部中断源
// 1. 初始化GPIO
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;  // 上升沿触发
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 2. 配置EXTI
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

3.3.2 中断服务函数的编写与测试

中断服务函数需在 stm32f1xx_it.c 中定义:

void EXTI0_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_0)
    {
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);  // 触发LED翻转
    }
}

逻辑分析:

  • EXTI0_IRQHandler :中断入口函数,调用HAL库处理。
  • HAL_GPIO_EXTI_Callback :回调函数,用户在此编写响应逻辑。
  • 使用中断方式相比轮询更高效,尤其适用于低功耗应用。
中断优先级配置表
中断线 优先级组 抢占优先级 子优先级
EXTI0 NVIC_PRIORITYGROUP_4 1 0
EXTI1 NVIC_PRIORITYGROUP_4 2 0

说明:

  • 抢占优先级决定中断是否能打断另一个中断。
  • 子优先级用于同抢占优先级下的中断排队顺序。

小结

本章详细讲解了STM32 GPIO的配置方法与基础输入输出操作,包括GPIO的多种工作模式、LED控制与按键检测、按键去抖策略以及GPIO中断的使用。通过代码示例和流程图的结合,帮助开发者掌握如何在实际项目中高效地使用GPIO资源。这些内容为后续更复杂的外设操作(如定时器、串口通信)奠定了坚实的基础。

4. 中断系统与定时器应用详解

中断和定时器是嵌套在STM32系统中的核心机制,它们在实现高效任务调度、外设响应、时间控制等方面扮演着至关重要的角色。本章将从底层机制入手,逐步解析STM32的中断控制器(NVIC)、中断优先级分组、中断嵌套机制,以及通用定时器的原理与配置方法。最后通过“中断+定时器”的联合应用案例,深入探讨如何构建高效的时间驱动型程序架构。

4.1 中断机制与NVIC控制器

STM32系列单片机基于ARM Cortex-M3内核,其强大的中断管理能力是其高性能运行的重要保障。NVIC(Nested Vectored Interrupt Controller)是Cortex-M3内核中的中断控制器,负责管理所有中断源的优先级、使能状态和响应顺序。

4.1.1 中断向量表与优先级分组

中断向量表结构

STM32的中断向量表是一个固定的地址表,位于内存的起始位置(默认为0x00000000)。每个中断源对应一个向量地址,该地址中存储的是该中断服务函数的入口地址。

例如:

中断号 中断源名称 地址偏移(字节)
0 复位中断 0x00
1 NMI(不可屏蔽中断) 0x04
2 硬件错误中断 0x08
5 SVCall(系统调用) 0x2C
13 PendSV(挂起服务调用) 0x3C
15 SysTick(系统滴答定时器) 0x44
中断优先级分组

ARM Cortex-M3支持最多16级抢占优先级(Preemption Priority)和子优先级(Subpriority)的划分。STM32通过 NVIC_PriorityGroupConfig() 函数进行优先级分组设置。

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

该函数将优先级分为4位,例如设置为 NVIC_PriorityGroup_2 时,表示:

  • 抢占优先级占2位(0~3)
  • 子优先级占2位(0~3)

说明:抢占优先级决定了中断是否可以打断当前正在执行的中断服务函数。子优先级用于在相同抢占优先级下决定响应顺序。

代码逻辑分析
  • NVIC_PriorityGroupConfig() 设置优先级分组方式,影响后续中断配置。
  • 优先级数值越小,优先级越高。
中断优先级配置流程
  1. 设置优先级分组
  2. 配置中断通道的抢占优先级和子优先级
  3. 启用中断通道
NVIC_InitTypeDef NVIC_InitStructure;

NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

4.1.2 中断嵌套与响应流程

中断响应流程

中断响应流程可以分为以下几个阶段:

graph TD
A[中断请求触发] --> B{当前是否允许中断?}
B -->|否| C[继续执行]
B -->|是| D[保存现场]
D --> E[查找中断向量表]
E --> F[跳转执行中断服务函数]
F --> G[中断处理完成]
G --> H[恢复现场]
H --> I[返回主程序]
中断嵌套机制

中断嵌套是指高优先级中断可以打断低优先级中断的执行。例如:

  • 优先级为0的中断可以打断优先级为1的中断服务函数。
  • 同一抢占优先级的中断之间不能嵌套。
实例分析:中断嵌套测试
void SysTick_Handler(void) {
    // 优先级为0,可打断其他中断
    GPIO_ToggleBits(GPIOC, GPIO_Pin_13);
}

void EXTI0_IRQHandler(void) {
    // 优先级为1
    Delay_ms(1000);
    EXTI_ClearITPendingBit(EXTI_Line0);
}

在这个例子中,当 EXTI0_IRQHandler 正在执行时, SysTick_Handler 会每隔固定时间打断它,执行一次LED切换。

4.2 定时器原理与配置

定时器是嵌入式系统中最常用的模块之一,广泛应用于定时、计数、PWM输出、输入捕获等场景。

4.2.1 通用定时器与基本定时功能

STM32F1系列提供了多达4个通用定时器(TIM2~TIM5),支持多种工作模式:

  • 向上计数模式
  • 向下计数模式
  • 中央对齐模式
  • PWM模式
  • 输入捕获模式
定时器基本参数计算

定时器时钟源通常来自APB1或APB2,假设APB1时钟为72MHz,TIM2预分频器为72-1,则定时器时钟频率为:

TIM_CLK = APB1_CLK / (TIM_Prescaler + 1) = 72MHz / 72 = 1MHz

若计数器自动重载值为1000-1,则定时器中断周期为:

Period = (TIM_ARR + 1) / TIM_CLK = 1000 / 1MHz = 1ms
基本定时器配置示例
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

TIM_TimeBaseStruct.TIM_Prescaler = 72 - 1;
TIM_TimeBaseStruct.TIM_Period = 1000 - 1;
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStruct);

TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM2, ENABLE);
代码逻辑分析
  • TIM_Prescaler :预分频系数,决定计数频率
  • TIM_Period :自动重载寄存器值,决定中断周期
  • TIM_ITConfig() :启用更新中断
  • TIM_Cmd() :启动定时器

4.2.2 PWM输出与输入捕获应用

PWM输出配置

PWM常用于电机控制、LED调光等场景。以TIM3通道1为例:

TIM_OCInitTypeDef TIM_OCStruct;

TIM_OCStruct.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCStruct.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCStruct.TIM_Pulse = 500;  // 占空比50%
TIM_OCStruct.TIM_OCPolarity = TIM_OCPolarity_High;

TIM_OC1Init(TIM3, &TIM_OCStruct);
TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM3, ENABLE);
TIM_Cmd(TIM3, ENABLE);
参数说明
  • TIM_OCMode_PWM1 :PWM模式1,计数器小于比较值输出高电平
  • TIM_Pulse :比较值,决定占空比
  • TIM_OCPolarity :输出极性,决定输出高/低电平时的行为
输入捕获配置

输入捕获用于测量外部信号的频率或脉宽。以TIM4通道1为例:

TIM_ICInitTypeDef TIM_ICStruct;

TIM_ICStruct.TIM_Channel = TIM_Channel_1;
TIM_ICStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICStruct.TIM_ICFilter = 0x0;

TIM_ICInit(TIM4, &TIM_ICStruct);
TIM_ICCmd(TIM4, TIM_Channel_1, ENABLE);
应用场景
  • 超声波测距:利用输入捕获测量回波时间
  • 编码器测速:通过脉冲宽度计算电机转速

4.3 中断+定时器联合应用案例

4.3.1 实现精确延时与定时任务调度

使用SysTick实现精确延时
volatile uint32_t msTicks = 0;

void SysTick_Handler(void) {
    if (msTicks != 0) {
        msTicks--;
    }
}

void Delay_ms(uint32_t delay) {
    msTicks = delay;
    while (msTicks != 0);
}
逻辑分析
  • SysTick_Handler 每1ms触发一次
  • Delay_ms() 通过循环等待 msTicks 减到0,实现精确延时
定时任务调度

利用定时器中断实现任务调度,如下:

void TIM2_IRQHandler(void) {
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
        TaskScheduler_Run();  // 调度任务
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}
任务调度逻辑示例
typedef void (*TaskFunc)(void);

typedef struct {
    TaskFunc func;
    uint32_t interval;
    uint32_t last_time;
} Task;

Task tasks[] = {
    {Task_LED_Blink, 500},
    {Task_ADC_Read, 1000}
};

void TaskScheduler_Run() {
    static uint32_t current_time = 0;
    current_time++;

    for (int i = 0; i < sizeof(tasks)/sizeof(Task); i++) {
        if (current_time - tasks[i].last_time >= tasks[i].interval) {
            tasks[i].func();
            tasks[i].last_time = current_time;
        }
    }
}
参数说明
  • current_time :当前系统时间计数(单位ms)
  • interval :任务执行间隔
  • last_time :上次执行时间

4.3.2 基于定时器的LED呼吸灯控制

原理分析

LED呼吸灯效果是通过调节PWM的占空比,实现亮度逐渐变化的效果。使用定时器中断动态调整占空比。

uint16_t pwm_value = 0;
uint8_t direction = 1;  // 1: increase, 0: decrease

void TIM3_IRQHandler(void) {
    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
        if (direction) {
            pwm_value += 10;
            if (pwm_value >= 1000) direction = 0;
        } else {
            pwm_value -= 10;
            if (pwm_value <= 0) direction = 1;
        }

        TIM_SetCompare1(TIM3, pwm_value);
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
    }
}
代码逻辑分析
  • pwm_value :当前PWM比较值
  • direction :控制变化方向
  • TIM_SetCompare1() :设置通道1的比较值,改变占空比
效果展示

通过上述代码,LED将呈现渐亮→渐暗→渐亮的呼吸灯效果,适用于低功耗提示、状态显示等场景。

本章从底层机制到实际应用,详细讲解了STM32中断系统与定时器的工作原理、配置方法以及联合应用场景。通过本章内容,读者应能够掌握中断优先级配置、定时器定时与PWM输出、中断嵌套处理等核心技能,并具备独立开发定时任务、PWM控制等项目的能力。

5. 串行通信协议配置与调试实践

在嵌入式系统中,串行通信是设备间数据交互的基础手段之一。STM32系列单片机集成了多种标准串行通信接口,如UART、SPI和I2C,广泛应用于传感器数据采集、模块通信、设备控制等场景。本章将深入探讨这些串行通信协议的原理、配置方法及调试技巧,帮助开发者掌握如何在STM32平台上高效地实现数据传输与设备控制。

5.1 UART通信原理与实现

UART(通用异步收发器)是最基础、最常用的串行通信方式之一。其核心原理是通过两根信号线(TXD发送、RXD接收)进行点对点的数据传输。

5.1.1 数据帧格式与波特率设置

UART通信的基本单位是 数据帧(Frame) ,一个标准的数据帧由以下几部分组成:

组成部分 描述
起始位(Start Bit) 1位低电平,表示数据帧开始
数据位(Data Bits) 5~8位,通常为8位
校验位(Parity Bit) 可选奇校验或偶校验
停止位(Stop Bit) 1~2位高电平,表示数据帧结束

波特率(Baud Rate) 表示每秒传输的比特数,常见的波特率有9600、115200等。波特率的设置需双方一致,否则会导致通信失败。

在STM32中,使用标准外设库(如HAL库)进行UART配置时,通常通过以下结构体进行设置:

UART_HandleTypeDef huart1;

void MX_USART1_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;
    if (HAL_UART_Init(&huart1) != HAL_OK)
    {
        Error_Handler();
    }
}

逐行解析:

  • Instance = USART1 :选择使用的是USART1模块。
  • BaudRate = 115200 :设定波特率为115200bps。
  • WordLength = 8B :每个数据帧为8位。
  • StopBits = 1 :使用1位停止位。
  • Parity = NONE :不使用校验位。
  • Mode = TX_RX :启用发送和接收功能。
  • HwFlowCtl = NONE :不使用硬件流控。
  • OverSampling = 16 :使用16倍过采样,提高采样精度。

5.1.2 串口收发与DMA传输优化

STM32支持使用 DMA(直接内存访问) 技术来提高串口数据传输效率,特别是在处理大量数据时,可以显著减轻CPU负担。

使用DMA发送串口数据的示例:
uint8_t txBuffer[] = "Hello STM32 UART with DMA\r\n";

HAL_UART_Transmit_DMA(&huart1, txBuffer, sizeof(txBuffer));

逻辑分析:

  • txBuffer :待发送的数据缓冲区。
  • sizeof(txBuffer) :发送数据长度。
  • 使用DMA发送时,数据会自动从内存搬运到串口寄存器,无需CPU干预。
配置DMA的流程:
  1. 在STM32CubeMX中配置UART1的DMA通道。
  2. 启用NVIC中断,用于DMA传输完成的回调。
  3. 编写DMA传输完成回调函数:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart == &huart1)
    {
        // 发送完成处理逻辑
    }
}

优势:

  • CPU占用率低,适合大数据量传输。
  • 实时性强,适用于高速通信场景。

5.2 SPI总线协议与设备通信

SPI(Serial Peripheral Interface)是一种高速同步串行通信协议,常用于与外部Flash、ADC、传感器等设备通信。SPI使用四根信号线:SCK(时钟)、MOSI(主发从收)、MISO(主收从发)、CS(片选)。

5.2.1 主从模式配置与数据传输

在STM32中,SPI模块可配置为主模式或从模式。在主模式下,STM32作为控制器,控制时钟输出和片选信号。

SPI主模式配置示例:
SPI_HandleTypeDef hspi1;

void MX_SPI1_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_16;
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
    hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
    hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    if (HAL_SPI_Init(&hspi1) != HAL_OK)
    {
        Error_Handler();
    }
}

逐行解析:

  • Mode = MASTER :设置为主模式。
  • Direction = 2LINES :全双工模式。
  • DataSize = 8BIT :每次传输8位数据。
  • CLKPolarity = LOW :空闲时SCK为低电平。
  • CLKPhase = 1EDGE :第一个时钟边沿采样。
  • NSS = SOFT :软件控制片选。
  • BaudRatePrescaler = 16 :SPI时钟分频为系统时钟/16。
  • FirstBit = MSB :高位先发。
  • TIMode = DISABLE :关闭TI模式。
SPI数据发送示例:
uint8_t txData[] = {0x01, 0x02, 0x03};
uint8_t rxData[3];

HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); // 拉低片选
HAL_SPI_TransmitReceive(&hspi1, txData, rxData, 3, HAL_MAX_DELAY);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);   // 拉高片选

流程图:

graph TD
    A[开始SPI通信] --> B[拉低CS]
    B --> C[发送数据到MOSI]
    C --> D[从MISO读取响应]
    D --> E[拉高CS结束通信]

5.2.2 与外部Flash、传感器通信实例

以W25Q128JV(外部Flash)为例,通过SPI协议进行读写操作:

读取Flash ID示例:
uint8_t cmd = 0x9F;
uint8_t rxData[3];

HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
HAL_SPI_Receive(&hspi1, rxData, 3, HAL_MAX_DELAY);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);

参数说明:

  • cmd = 0x9F :读取设备ID命令。
  • rxData :接收的3字节设备ID。

应用说明:

该方法适用于各种SPI接口的传感器和存储设备,如温湿度传感器、ADC芯片、SD卡等。

5.3 I2C总线协议与多设备通信

I2C是一种双线制串行通信协议,使用SDA(数据线)和SCL(时钟线)进行通信,支持多主多从结构,常用于连接低速外设如EEPROM、实时时钟、传感器等。

5.3.1 主机模式下的读写操作

STM32的I2C模块支持主机和从机模式,常见使用为主机模式。

I2C初始化代码示例:
I2C_HandleTypeDef hi2c1;

void MX_I2C1_Init(void)
{
    hi2c1.Instance = I2C1;
    hi2c1.Init.ClockSpeed = 100000;
    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;
    if (HAL_I2C_Init(&hi2c1) != HAL_OK)
    {
        Error_Handler();
    }
}

逐行解析:

  • ClockSpeed = 100000 :设置时钟频率为100kHz。
  • DutyCycle = 2 :时钟高电平占空比为1/2。
  • AddressingMode = 7BIT :使用7位地址模式。
  • DualAddressMode = DISABLE :关闭双地址模式。
  • GeneralCallMode = DISABLE :关闭通用地址响应。
  • NoStretchMode = DISABLE :允许时钟拉伸。
写入EEPROM示例:
uint8_t buffer[2] = {0x00, 0x55};  // 地址0x00写入0x55
HAL_I2C_Master_Transmit(&hi2c1, (0x50 << 1), buffer, 2, HAL_MAX_DELAY);
从EEPROM读取数据:
uint8_t readData;
HAL_I2C_Master_Transmit(&hi2c1, (0x50 << 1), &buffer[0], 1, HAL_MAX_DELAY);
HAL_I2C_Master_Receive(&hi2c1, (0x50 << 1), &readData, 1, HAL_MAX_DELAY);

流程图:

graph TD
    A[开始I2C通信] --> B[发送从机地址+写位]
    B --> C[发送寄存器地址]
    C --> D[发送从机地址+读位]
    D --> E[读取寄存器值]
    E --> F[结束通信]

5.3.2 多从机通信与地址冲突解决

I2C总线支持多个从机设备连接,每个设备有唯一地址(7位或10位)。当多个设备地址冲突时,可通过以下方式解决:

解决方案:
  1. 硬件修改地址引脚 :部分I2C设备提供地址选择引脚(如A0、A1),通过接地或接电源改变地址。
  2. 软件模拟I2C :使用GPIO模拟I2C时序,可为每个设备配置独立的SCL/SDA引脚。
  3. 使用I2C多路复用器 :如TCA9548A,可将一条I2C总线扩展为多个通道,避免地址冲突。
示例:使用TCA9548A扩展I2C通道
uint8_t channel = 0x01;  // 选择通道1
HAL_I2C_Master_Transmit(&hi2c1, 0x70 << 1, &channel, 1, HAL_MAX_DELAY);

参数说明:

  • 0x70 :TCA9548A的I2C地址。
  • channel :选择的通道号。

优势:

  • 可连接多个相同地址的设备。
  • 简化硬件设计,提高系统扩展性。

本章通过UART、SPI和I2C三种串行通信协议的详细讲解与实例演示,帮助开发者掌握STM32平台上的通信机制、配置方法与调试技巧。下一章将深入探讨模拟信号处理与CAN总线通信,进一步扩展STM32在工业控制中的应用场景。

6. 模拟信号处理与CAN总线通信

在现代嵌入式系统中,模拟信号的采集与处理、以及设备间的高速通信是构建复杂控制系统的核心能力。STM32系列单片机内置了高性能的ADC(模数转换器)和DAC(数模转换器)模块,并集成了CAN(Controller Area Network)总线控制器,使其在工业控制、汽车电子和物联网领域具有广泛的应用场景。本章将围绕STM32的模拟信号处理模块与CAN通信功能,深入讲解其配置方法、编程实现及联合应用策略。

6.1 ADC与DAC模块配置

STM32的ADC模块具备高精度、多通道采集能力,支持单次、连续、扫描等多种工作模式,适用于传感器数据采集、电压检测等应用场景。DAC模块则用于生成模拟信号输出,常用于信号发生器、波形合成等场合。

6.1.1 模拟输入采集与电压转换

ADC模块的基本结构

STM32的ADC是一个12位逐次逼近型模数转换器,支持最多16个外部通道和多个内部通道(如温度传感器、内部参考电压)。其主要特性包括:

  • 12位分辨率(精度为Vref+/4096)
  • 多通道扫描模式
  • 自动校准功能
  • 支持DMA传输,提升数据采集效率
  • 多种触发源(软件触发、定时器触发等)
配置步骤

以STM32F4系列为例,使用CubeMX配置ADC1的通道0(PA0)进行电压采集:

  1. 打开STM32CubeMX,选择芯片型号,进入Pinout视图。
  2. 设置PA0为ADC1_IN0功能。
  3. 在ADC1配置页面选择“Single Conversion Mode”。
  4. 启用ADC1的DMA通道,提升采集效率。
  5. 生成初始化代码。
示例代码与逻辑分析
// 在main.c中添加ADC采集代码
uint16_t adc_value;
float voltage;

HAL_ADC_Start(&hadc1);  // 启动ADC转换
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);  // 等待转换完成
adc_value = HAL_ADC_GetValue(&hadc1);  // 获取ADC值
voltage = (adc_value * 3.3) / 4095;  // 转换为实际电压值

代码逻辑分析:
- HAL_ADC_Start 启动一次ADC转换。
- HAL_ADC_PollForConversion 等待转换完成,适合低频率采样场景。
- HAL_ADC_GetValue 获取12位ADC原始值。
- 使用公式将ADC值转换为电压:
$$
V_{out} = \frac{ADC_{value} \times V_{ref+}}{4095}
$$

参数说明
参数 描述
Vref+ 参考电压,通常为3.3V
ADC_value 12位ADC转换结果,范围0~4095
HAL_MAX_DELAY 无限等待,适用于调试阶段
采集数据可视化(可选)

可以将采集的电压值通过串口发送到PC端,使用串口助手(如XCOM)进行数据可视化。

6.1.2 数模输出控制与波形生成

DAC模块的基本结构

STM32的DAC模块通常为12位电压输出型,支持两个独立通道(DAC1、DAC2),可配置为:

  • 单次模式
  • 缓冲/非缓冲输出
  • 正弦波、三角波等波形生成(需配合定时器)
配置步骤

以DAC1通道1(PA4)为例,生成一个正弦波:

  1. CubeMX中设置PA4为DAC1_OUT1。
  2. 配置DAC1为“Normal Mode”。
  3. 启用定时器触发DAC更新(如TIM6)。
  4. 生成初始化代码。
示例代码与逻辑分析
const uint16_t sine_table[100] = {
    // 正弦波表数据,由0~4095范围的12位数值构成
    2048, 2148, 2245, ..., 2048  // 假设已预生成
};

int index = 0;

// 在main中启动DAC
HAL_DAC_Start(&hdac, DAC_CHANNEL_1);

// 定时器中断服务函数中更新DAC值
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim == &htim6) {
        HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, sine_table[index]);
        index = (index + 1) % 100;
    }
}

代码逻辑分析:
- HAL_DAC_Start 启动DAC通道。
- HAL_DAC_SetValue 设置DAC输出值。
- 使用定时器中断实现周期性更新DAC值,从而输出波形。
- sine_table 是一个预先计算好的正弦波数值表。

波形输出参数说明
参数 描述
DAC_ALIGN_12B_R 12位右对齐
sine_table 存储正弦波形的数组
TIM6 定时器用于控制波形频率
波形示意图(mermaid)
graph TD
A[DAC模块] --> B[正弦波形]
B --> C[PA4输出]
C --> D[示波器显示波形]

6.2 CAN总线协议基础与配置

CAN(Controller Area Network)总线是一种广泛应用于工业控制和汽车电子中的高效串行通信协议,具备高可靠性和抗干扰能力。STM32内置的CAN控制器支持标准帧(11位ID)和扩展帧(29位ID),可实现多节点通信。

6.2.1 CAN控制器与收发器连接

硬件连接说明

STM32的CAN控制器需通过CAN收发器(如TJA1050)连接至物理总线。常见连接如下:

STM32引脚 功能 外设连接
CAN_RX 接收引脚 TJA1050 TXD
CAN_TX 发送引脚 TJA1050 RXD
VCC 电源 5V电源
GND GND
CAN通信拓扑结构(mermaid)
graph LR
MCU1((STM32)) --> CAN_BUS
MCU2((STM32)) --> CAN_BUS
MCU3((PLC)) --> CAN_BUS
CAN_BUS --> TERMINATOR(终端电阻 120Ω)

6.2.2 报文发送与接收机制

配置步骤

使用CubeMX配置CAN1:

  1. 设置CAN1为“Normal Mode”。
  2. 配置波特率为500kbps。
  3. 设置过滤器为“Accept All”以便接收所有报文。
  4. 生成初始化代码。
示例代码:发送CAN报文
CAN_TxHeaderTypeDef TxHeader;
uint8_t TxData[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
uint32_t TxMailbox;

TxHeader.StdId = 0x123;      // 标准ID
TxHeader.ExtId = 0x01;       // 扩展ID(可选)
TxHeader.IDE = CAN_ID_STD;   // 使用标准帧
TxHeader.RTR = CAN_RTR_DATA; // 数据帧
TxHeader.DLC = 8;            // 数据长度

HAL_CAN_AddTxMessage(&hcan1, &TxHeader, TxData, &TxMailbox);

代码逻辑分析:
- CAN_TxHeaderTypeDef 定义了CAN帧头结构。
- StdId 是标准ID,用于标识报文。
- DLC 表示数据长度,最大为8字节。
- HAL_CAN_AddTxMessage 将报文加入发送邮箱,由硬件自动发送。

示例代码:接收CAN报文
CAN_RxHeaderTypeDef RxHeader;
uint8_t RxData[8];

if (HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &RxHeader, RxData, HAL_MAX_DELAY) == HAL_OK) {
    // 成功接收到报文
    printf("Received ID: 0x%X, Data: 0x%X 0x%X 0x%X 0x%X\n",
           RxHeader.StdId, RxData[0], RxData[1], RxData[2], RxData[3]);
}

代码逻辑分析:
- HAL_CAN_GetRxMessage 从FIFO0中读取接收到的报文。
- RxHeader 包含帧ID、帧类型等信息。
- RxData 是接收到的数据内容。

CAN通信参数对照表
参数 描述
StdId 标准帧ID(11位)
DLC 数据长度(0~8字节)
RTR 远程帧请求位
IDE 帧类型标识(标准/扩展)
FIFO 接收缓存队列

6.3 模拟信号与CAN通信联合应用

在工业控制系统中,常常需要将采集的模拟信号通过CAN总线传输到远程设备。例如,采集温度传感器信号并通过CAN总线上传至主控单元。

6.3.1 采集数据并通过CAN总线发送

应用场景描述
  • 使用ADC采集PT100传感器电压信号。
  • 将电压值转换为温度值。
  • 通过CAN总线将温度数据发送至PLC或上位机。
实现步骤
  1. 配置ADC通道采集传感器电压。
  2. 使用CAN控制器配置通信参数。
  3. 在主循环中读取ADC值,转换为温度。
  4. 构建CAN报文,发送至指定ID节点。
示例代码
float voltage, temperature;
uint8_t can_data[4];

while (1) {
    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
    adc_value = HAL_ADC_GetValue(&hadc1);
    voltage = (adc_value * 3.3) / 4095;

    // 假设传感器为线性关系:温度 = voltage * 100
    temperature = voltage * 100;

    // 将温度值转换为四个字节
    memcpy(can_data, &temperature, sizeof(float));

    // 构建CAN报文并发送
    TxHeader.StdId = 0x200;
    TxHeader.DLC = 4;
    HAL_CAN_AddTxMessage(&hcan1, &TxHeader, can_data, &TxMailbox);

    HAL_Delay(1000);  // 每秒发送一次
}

代码逻辑分析:
- memcpy 将浮点型温度值转换为4个字节的数据。
- can_data 用于存储待发送的温度数据。
- 每秒钟采集一次ADC值并发送CAN报文。

数据格式说明表
字节位置 数据含义
can_data[0] 温度值字节0
can_data[1] 温度值字节1
can_data[2] 温度值字节2
can_data[3] 温度值字节3

6.3.2 构建简易工业控制通信系统

系统架构设计

本系统由多个STM32节点组成,分别负责采集数据、执行控制指令,并通过CAN总线实现节点间通信。

graph TD
A[节点1: ADC采集] --> C[CAN总线]
B[节点2: 控制执行] --> C
C --> D[主控节点: 数据汇总与控制]
主要功能模块
模块 功能描述
ADC采集模块 实时采集传感器信号
DAC输出模块 输出控制信号
CAN通信模块 节点间数据传输
控制逻辑模块 根据数据做出响应
系统运行流程
  1. 节点1采集传感器数据,通过CAN总线发送。
  2. 主控节点接收数据并分析。
  3. 若需要控制,主控节点发送控制指令至节点2。
  4. 节点2接收指令并执行动作(如开启电机)。
通信数据结构定义
typedef struct {
    uint16_t sensor_id;
    float value;
} SensorData;

typedef struct {
    uint16_t control_id;
    uint8_t command;
} ControlCommand;

该结构体定义了节点间通信的数据格式,便于统一处理。

本章通过ADC/DAC与CAN通信的联合应用,展示了STM32在工业控制中的实际用途。结合模拟信号采集与高速通信能力,开发者可以构建出功能强大、稳定性高的嵌入式系统。下一章将进入RTOS与低功耗设计的实践环节,进一步拓展STM32的应用边界。

7. 实时操作系统与低功耗项目实战

7.1 FreeRTOS在STM32上的移植

7.1.1 任务创建与调度机制

在嵌入式系统中,使用实时操作系统(RTOS)可以提高系统的并发处理能力与任务调度效率。FreeRTOS 是一个轻量级、可移植、可裁剪的实时操作系统,广泛应用于 STM32 系列单片机。

以 STM32F407 为例,使用 STM32CubeIDE 搭建 FreeRTOS 工程的步骤如下:

  1. 使用 STM32CubeMX 配置 RCC、SYS、GPIO 等基本外设。
  2. 在 Middleware 中启用 FreeRTOS,选择任务调度方式(抢占式或协作式)。
  3. 生成初始化代码,进入 STM32CubeIDE 编写任务函数。

下面是一个简单的任务创建示例:

#include "FreeRTOS.h"
#include "task.h"

void Task1(void *pvParameters) {
    while(1) {
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);  // 翻转LED
        vTaskDelay(pdMS_TO_TICKS(500));         // 延时500ms
    }
}

void Task2(void *pvParameters) {
    while(1) {
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_6);  // 翻转另一个LED
        vTaskDelay(pdMS_TO_TICKS(1000));        // 延时1s
    }
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();

    xTaskCreate(Task1, "Task1", 128, NULL, 1, NULL);
    xTaskCreate(Task2, "Task2", 128, NULL, 1, NULL);

    vTaskStartScheduler();

    while(1) {}
}
  • xTaskCreate :创建任务函数,参数依次为任务函数指针、任务名、堆栈大小、传入参数、优先级、任务句柄。
  • vTaskDelay :延时函数,参数为 ticks 数,通过 pdMS_TO_TICKS 转换毫秒。

FreeRTOS 使用优先级抢占调度策略,优先级高的任务会中断当前运行的低优先级任务。

7.1.2 任务间通信与同步机制

FreeRTOS 提供了多种任务间通信机制,如队列(Queue)、信号量(Semaphore)、互斥量(Mutex)等。

以下示例展示如何使用队列进行任务间通信:

QueueHandle_t xQueue;

void Task1(void *pvParameters) {
    uint8_t ucValueToSend = 0;
    while(1) {
        xQueueSend(xQueue, &ucValueToSend, portMAX_DELAY);
        ucValueToSend++;
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void Task2(void *pvParameters) {
    uint8_t ucReceivedValue;
    while(1) {
        if(xQueueReceive(xQueue, &ucReceivedValue, portMAX_DELAY)) {
            // 接收成功,处理数据
        }
    }
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    xQueue = xQueueCreate(10, sizeof(uint8_t));

    xTaskCreate(Task1, "Task1", 128, NULL, 1, NULL);
    xTaskCreate(Task2, "Task2", 128, NULL, 1, NULL);

    vTaskStartScheduler();

    while(1) {}
}
  • xQueueCreate :创建队列,参数为长度和每个元素大小。
  • xQueueSend :发送数据到队列。
  • xQueueReceive :从队列中接收数据。

7.2 低功耗与电源管理策略

7.2.1 STM32睡眠模式与唤醒机制

STM32 提供了多种低功耗模式,包括:

模式名称 特点 功耗
睡眠模式(Sleep) CPU 停止,外设继续运行,可被中断唤醒
深度睡眠(Deep Sleep) CPU 和大部分外设停止,保留 SRAM,唤醒需复位 极低
待机模式(Standby) 仅保留唤醒电路,系统完全断电,唤醒后需重新初始化 最低

以下为进入深度睡眠模式的代码示例:

void EnterDeepSleep(void) {
    HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
    // 唤醒后需要重新配置系统时钟
    SystemClock_Config();
}
  • PWR_LOWPOWERREGULATOR_ON :低功耗稳压器开启。
  • PWR_STOPENTRY_WFI :通过 WFI 指令进入休眠。

7.2.2 降低功耗的软硬件优化技巧

软件优化:

  • 合理使用低功耗模式,例如在等待外部事件时进入休眠。
  • 减少外设使用频率,如降低 ADC 采样率、关闭未使用的模块。
  • 使用中断驱动方式,避免轮询浪费 CPU 时间。

硬件优化:

  • 使用低功耗传感器与外围模块(如低功耗 Wi-Fi 模块)。
  • 设计合理的电源管理电路,如使用 LDO 降低电压。
  • 对未使用的引脚进行配置,避免浮空状态造成漏电流。

7.3 综合项目实践:温湿度监测系统

7.3.1 系统架构设计与硬件连接

本项目基于 STM32F407,结合 DHT11 温湿度传感器与 ESP8266 Wi-Fi 模块,实现温湿度数据采集并通过 MQTT 协议上传至服务器。系统整体结构如下:

graph TD
    A[STM32F407] --> B(DHT11 温湿度传感器)
    A --> C(ESP8266 Wi-Fi 模块)
    C --> D[(MQTT Broker)]
  • DHT11 :接在 PA0 引脚,用于采集温湿度数据。
  • ESP8266 :通过 UART2(PA2-TX, PA3-RX)与 STM32 通信,用于连接 Wi-Fi 并上传数据。
  • 低功耗设计 :定时采集数据,采集完成后进入深度睡眠模式。

7.3.2 数据采集、传输与低功耗管理实现

void vTempTask(void *pvParameters) {
    float temperature, humidity;
    while(1) {
        dht11_read(&temperature, &humidity);  // 读取温湿度
        send_to_mqtt(temperature, humidity);  // 发送至MQTT
        HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
        SystemClock_Config();  // 唤醒后重新配置时钟
        vTaskDelay(pdMS_TO_TICKS(30000));  // 每30秒采集一次
    }
}
  • dht11_read :自定义函数,实现 DHT11 协议解析。
  • send_to_mqtt :通过 ESP8266 发送 MQTT 消息,需实现 AT 指令解析与连接逻辑。
  • 低功耗机制 :每次采集后进入深度睡眠,唤醒后重新运行任务。

此外,可以使用 RTC 定时唤醒功能替代 vTaskDelay ,进一步降低功耗。

void Configure_RTC_Alarm(void) {
    RTC_AlarmTypeDef sAlarm = {0};
    sAlarm.Alarm = RTC_ALARM_A;
    sAlarm.AlarmTime.Seconds = 0x30;  // 30秒后唤醒
    sAlarm.AlarmMask = RTC_ALARMMASK_HOURS | RTC_ALARMMASK_MINUTES;
    sAlarm.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_ALL;
    sAlarm.AlarmDateWeekDaySel = RTC_ALARMDATEWEEKDAYSEL_DATE;
    sAlarm.AlarmDateWeekDay = 0x1;
    sAlarm.AlarmTime.TimeFormat = RTC_HOURFORMAT12_AM;
    HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BCD);
}

该配置将 RTC 闹钟设置为 30 秒后唤醒系统,唤醒后执行下一次采集任务。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:STM32是意法半导体推出的基于ARM Cortex-M内核的高性能、低功耗微控制器系列,广泛应用于物联网、电机控制和人机交互等领域。本教程专为初学者设计,涵盖STM32F103核心知识,从开发环境搭建、GPIO操作、中断定时器、串口通信到ADC/DAC、CAN总线、USB接口等外设使用,并引入RTOS和实战项目,帮助学习者掌握嵌入式系统开发全流程。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐