1. 项目概述:STM32固件程序设计实验
这个实验项目由三位同学(20165226、20165310、20165315)合作完成,主要围绕STM32微控制器的固件程序设计展开。固件程序是嵌入式系统的核心,直接控制硬件外设的行为,决定了设备的全部功能特性。本次实验将使用Keil MDK开发环境和JLink调试器,重点探索GPIO和UART这两个最基础也最重要的外设接口。
在嵌入式开发领域,STM32系列因其丰富的外设资源和完整的生态系统而广受欢迎。根据我的经验,掌握GPIO和UART的使用是嵌入式开发的基石——GPIO负责最简单的数字输入输出,而UART则是设备间通信的最常用接口。这两个外设虽然基础,但使用不当会导致各种难以排查的问题。
2. 开发环境搭建与配置
2.1 Keil MDK安装与工程创建
Keil MDK(Microcontroller Development Kit)是ARM官方推荐的嵌入式开发环境,特别适合STM32系列微控制器。安装时需要注意:
- 下载MDK最新版本(建议5.38以上)
- 安装时勾选所有ARM Cortex-M相关组件
- 安装对应的设备支持包(Device Family Pack)
创建新工程时,选择正确的芯片型号至关重要。我曾经遇到过因为选错芯片型号导致程序无法正常运行的情况。例如,STM32F103C8和STM32F103CB虽然引脚兼容,但Flash大小不同,选错会导致链接错误。
提示:在Project → Options for Target → Target选项卡中,务必确认选择的芯片型号与实际硬件完全一致。
2.2 JLink驱动安装与配置
JLink是Segger公司生产的高性能调试器,相比ST-Link有更好的兼容性和稳定性。安装步骤:
- 从Segger官网下载最新驱动包
- 安装时选择完整安装(包括USB驱动和调试组件)
- 连接JLink到电脑,设备管理器应显示"SEGGER J-Link"设备
在Keil中配置JLink:
- 进入Project → Options for Target → Debug
- 选择"J-Link / J-Trace Cortex"作为调试器
- 在Settings中,Port选择SW,速度设为4MHz
我遇到过JLink无法识别设备的情况,通常是因为:
- 目标板供电不足(确保3.3V稳定)
- SWD线序接错(SWDIO、SWCLK、GND必须正确连接)
- 芯片处于复位状态(检查NRST引脚电平)
3. GPIO配置与应用
3.1 GPIO工作模式详解
STM32的GPIO有8种工作模式,每种模式适用于不同场景:
输入模式
- 浮空输入:用于数字信号输入,如按键检测
- 上拉输入:内置上拉电阻,节省外部元件
- 下拉输入:内置下拉电阻,节省外部元件
输出模式
- 推挽输出:标准数字输出,可驱动LED等负载
- 开漏输出:用于I2C等需要线与的场景
复用功能
- 复用推挽:用于UART、SPI等外设
- 复用开漏:用于I2C等外设
模拟模式
- 用于ADC输入或DAC输出
在实验中,我们主要使用推挽输出模式控制LED,上拉输入模式检测按键。
3.2 GPIO初始化代码实现
使用HAL库初始化GPIO的典型代码如下:
GPIO_InitTypeDef GPIO_InitStruct = {0}; // 使能GPIO端口时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); // 配置GPIO引脚 GPIO_InitStruct.Pin = GPIO_PIN_5; // 选择PA5引脚 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出 GPIO_InitStruct.Pull = GPIO_NOPULL; // 无上下拉 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速模式 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 初始化GPIO在实际项目中,我习惯将GPIO初始化代码封装成单独的函数,提高代码可读性和复用性。例如:
void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_5; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 默认关闭LED HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); }3.3 GPIO使用注意事项
时钟使能:在使用任何GPIO前,必须先使能对应的GPIO端口时钟。这是新手常犯的错误。
引脚冲突:避免将同一个引脚同时配置为多个功能。例如,不能将PA9同时用作GPIO输出和UART_TX。
驱动能力:GPIO的输出电流有限(通常8-20mA),驱动大电流负载时需要添加晶体管或MOSFET。
去抖动处理:当GPIO用于按键检测时,必须添加硬件或软件去抖动措施。我通常使用定时器实现20ms的软件去抖动。
4. UART通信实现
4.1 UART基本原理
UART(Universal Asynchronous Receiver/Transmitter)是一种异步串行通信协议,特点包括:
- 全双工通信
- 不需要时钟信号
- 可配置的波特率
- 起始位、停止位和可选的校验位
在STM32中,USART(Universal Synchronous/Asynchronous Receiver/Transmitter)模块支持UART模式,还支持同步通信和LIN、IrDA等协议。
4.2 UART初始化配置
使用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(); } }在初始化前,还需要配置GPIO引脚为复用功能:
GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART1_CLK_ENABLE(); // 配置USART1 TX (PA9) 和 RX (PA10) GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate = GPIO_AF7_USART1; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);4.3 UART数据收发
HAL库提供了多种UART数据传输方式:
- 阻塞式传输:
// 发送数据 HAL_UART_Transmit(&huart1, (uint8_t*)"Hello\r\n", 7, HAL_MAX_DELAY); // 接收数据 uint8_t rx_data[10]; HAL_UART_Receive(&huart1, rx_data, 10, HAL_MAX_DELAY);- 中断方式:
// 启动接收中断 HAL_UART_Receive_IT(&huart1, rx_data, 10); // 在中断回调函数中处理数据 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1) { // 处理接收到的数据 // 重新启动接收 HAL_UART_Receive_IT(&huart1, rx_data, 10); } }- DMA方式(适合大数据量传输):
// 配置DMA __HAL_RCC_DMA2_CLK_ENABLE(); hdma_usart1_rx.Instance = DMA2_Stream2; hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4; // ...其他DMA参数配置 HAL_DMA_Init(&hdma_usart1_rx); // 关联DMA到UART __HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx); // 启动DMA接收 HAL_UART_Receive_DMA(&huart1, rx_data, 10);在实际项目中,我通常使用中断+DMA的方式实现高效的串口通信。这种方式既能减轻CPU负担,又能及时响应数据接收。
4.4 UART常见问题排查
无数据收发:
- 检查波特率设置是否与对方设备一致
- 确认TX和RX线是否交叉连接
- 检查GPIO是否配置为正确的复用功能
数据错误:
- 检查时钟配置是否正确
- 确认双方的数据格式(数据位、停止位、校验位)一致
- 检查是否有电磁干扰(可降低波特率测试)
接收中断不触发:
- 确认NVIC中断已使能
- 检查接收缓冲区是否溢出
- 确保没有更高优先级的中断阻塞UART中断
我曾经遇到过一个棘手的问题:UART只能发送不能接收。经过仔细排查,发现是RX引脚被意外配置为了输出模式。这个经验告诉我,在调试外设时,一定要仔细检查GPIO的配置。
5. 实验过程与结果分析
5.1 实验一:GPIO控制LED闪烁
实验目标:通过GPIO控制板载LED实现1Hz的闪烁。
实现步骤:
- 初始化LED对应的GPIO引脚为输出模式
- 在主循环中交替设置引脚电平
- 添加适当的延时
关键代码:
while (1) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); HAL_Delay(500); // 500ms延时 }实验结果:LED按预期以1Hz频率闪烁,验证了GPIO输出功能正常。
5.2 实验二:UART回显测试
实验目标:通过UART实现数据回显,即发送什么数据就返回什么数据。
实现步骤:
- 初始化UART外设
- 配置接收中断
- 在接收中断回调函数中将接收到的数据发送回去
关键代码:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1) { // 回显接收到的数据 HAL_UART_Transmit(&huart1, &rx_data, 1, HAL_MAX_DELAY); // 重新启动接收 HAL_UART_Receive_IT(&huart1, &rx_data, 1); } }实验结果:通过串口助手发送任意字符,设备都能正确回显,验证了UART收发功能正常。
5.3 实验三:按键控制LED
实验目标:通过按键控制LED的亮灭状态。
实现步骤:
- 初始化按键对应的GPIO引脚为输入模式(上拉)
- 初始化LED对应的GPIO引脚为输出模式
- 在主循环中检测按键状态并控制LED
关键代码:
while (1) { if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) { // 按键按下(低电平) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); } else { // 按键释放 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); } }实验结果:按下按键时LED亮起,释放按键时LED熄灭,验证了GPIO输入功能正常。
6. 项目总结与经验分享
通过这次固件程序设计实验,我们深入掌握了STM32的GPIO和UART外设的使用方法。以下是我在实际开发中总结的一些经验:
调试技巧:
- 当程序不按预期运行时,首先检查所有外设的时钟是否使能
- 使用GPIO翻转和逻辑分析仪可以快速定位程序卡死的位置
- 在UART通信中,先测试发送功能,再测试接收功能
代码优化:
- 将外设初始化代码封装成单独的函数,提高代码可读性
- 使用HAL库的回调机制,避免轮询方式浪费CPU资源
- 对于频繁调用的函数,考虑使用寄存器操作代替库函数提高效率
常见陷阱:
- 忘记调用SystemClock_Config()函数导致外设时钟不正确
- 没有正确处理中断优先级导致系统不稳定
- GPIO速度配置不当导致信号完整性问题
扩展建议:
- 尝试使用CubeMX工具生成初始化代码,提高开发效率
- 探索DMA方式实现UART数据传输,减轻CPU负担
- 研究更复杂的中断处理机制,如嵌套中断
在实际项目中,我建议采用模块化的编程方式,将GPIO、UART等外设的驱动代码与业务逻辑分离。这样不仅便于调试,也方便后续功能扩展。例如,可以创建一个uart_comm.c文件专门处理所有UART通信相关的功能,通过清晰的接口与主程序交互。
最后,嵌入式开发是一个需要理论与实践相结合的领域。只有通过不断的实验和调试,才能真正掌握这些外设的使用技巧。希望本文的内容能帮助读者少走弯路,快速掌握STM32固件程序设计的核心要点。