零基础入门STM32单片机开发完整教程
该结构体定义了节点间通信的数据格式,便于统一处理。本章通过ADC/DAC与CAN通信的联合应用,展示了STM32在工业控制中的实际用途。结合模拟信号采集与高速通信能力,开发者可以构建出功能强大、稳定性高的嵌入式系统。下一章将进入RTOS与低功耗设计的实践环节,进一步拓展STM32的应用边界。
简介:STM32是意法半导体推出的基于ARM Cortex-M内核的高性能、低功耗微控制器系列,广泛应用于物联网、电机控制和人机交互等领域。本教程专为初学者设计,涵盖STM32F103核心知识,从开发环境搭建、GPIO操作、中断定时器、串口通信到ADC/DAC、CAN总线、USB接口等外设使用,并引入RTOS和实战项目,帮助学习者掌握嵌入式系统开发全流程。 
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 安装步骤
- 下载安装包 :访问Keil官网,下载对应版本的MDK(建议选择MDK-ARM版本)。
- 运行安装程序 :双击安装包,选择安装路径。
- 注册License :安装完成后,打开Keil uVision,进入“File” -> “License Management”,复制CID码,访问Keil官网生成License。
- 安装设备支持包 :根据所使用的STM32型号,下载对应的Device Family Pack(DFP)并安装。
STM32CubeIDE 安装步骤
- 下载安装包 :访问ST官网,下载最新版本的STM32CubeIDE。
- 解压并运行 :STM32CubeIDE为绿色软件,解压后直接运行
stm32cubeide.exe即可。 - 安装插件(可选) :启动后可通过“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工程]
操作步骤:
- 选择芯片型号 :点击“Start My Project”,在搜索框中输入目标芯片型号(如STM32F407VG)。
- 配置引脚功能 :进入“Pinout & Configuration”页面,点击引脚设置其功能,如GPIO、USART、SPI等。
- 配置系统时钟 :进入“Clock Configuration”页面,设置HSE、PLL等参数,生成系统时钟频率。
- 启用外设模块 :在“Configuration”页面中启用所需外设(如TIM、ADC、I2C等),并配置其参数。
- 生成代码 :点击“Project” -> “Generate Code”,选择目标IDE(如Keil、STM32CubeIDE),设置工程路径后生成代码。
- 导出工程 :生成完成后,打开对应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为例):
- 将ST-Link调试器连接至目标板的SWD接口。
- 打开STM32CubeIDE,点击“Run” -> “Debug As” -> “STM32 Cortex-M C/C++ Application”。
- IDE会自动编译、下载程序,并进入调试界面。
- 使用“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线。
配置步骤:
- 配置GPIO为输入模式(如上拉)。
- 配置EXTI线与GPIO引脚关联。
- 配置NVIC中断优先级并使能。
- 编写中断服务函数。
示例代码:配置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()设置优先级分组方式,影响后续中断配置。- 优先级数值越小,优先级越高。
中断优先级配置流程
- 设置优先级分组
- 配置中断通道的抢占优先级和子优先级
- 启用中断通道
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的流程:
- 在STM32CubeMX中配置UART1的DMA通道。
- 启用NVIC中断,用于DMA传输完成的回调。
- 编写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位)。当多个设备地址冲突时,可通过以下方式解决:
解决方案:
- 硬件修改地址引脚 :部分I2C设备提供地址选择引脚(如A0、A1),通过接地或接电源改变地址。
- 软件模拟I2C :使用GPIO模拟I2C时序,可为每个设备配置独立的SCL/SDA引脚。
- 使用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)进行电压采集:
- 打开STM32CubeMX,选择芯片型号,进入Pinout视图。
- 设置PA0为ADC1_IN0功能。
- 在ADC1配置页面选择“Single Conversion Mode”。
- 启用ADC1的DMA通道,提升采集效率。
- 生成初始化代码。
示例代码与逻辑分析
// 在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)为例,生成一个正弦波:
- CubeMX中设置PA4为DAC1_OUT1。
- 配置DAC1为“Normal Mode”。
- 启用定时器触发DAC更新(如TIM6)。
- 生成初始化代码。
示例代码与逻辑分析
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:
- 设置CAN1为“Normal Mode”。
- 配置波特率为500kbps。
- 设置过滤器为“Accept All”以便接收所有报文。
- 生成初始化代码。
示例代码:发送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或上位机。
实现步骤
- 配置ADC通道采集传感器电压。
- 使用CAN控制器配置通信参数。
- 在主循环中读取ADC值,转换为温度。
- 构建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采集传感器数据,通过CAN总线发送。
- 主控节点接收数据并分析。
- 若需要控制,主控节点发送控制指令至节点2。
- 节点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 工程的步骤如下:
- 使用 STM32CubeMX 配置 RCC、SYS、GPIO 等基本外设。
- 在 Middleware 中启用 FreeRTOS,选择任务调度方式(抢占式或协作式)。
- 生成初始化代码,进入 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 秒后唤醒系统,唤醒后执行下一次采集任务。
简介:STM32是意法半导体推出的基于ARM Cortex-M内核的高性能、低功耗微控制器系列,广泛应用于物联网、电机控制和人机交互等领域。本教程专为初学者设计,涵盖STM32F103核心知识,从开发环境搭建、GPIO操作、中断定时器、串口通信到ADC/DAC、CAN总线、USB接口等外设使用,并引入RTOS和实战项目,帮助学习者掌握嵌入式系统开发全流程。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)