1. STM32开发入门:从零开始掌握基础外设
第一次接触STM32的开发板时,我被它密密麻麻的引脚和复杂的数据手册吓到了。但经过几个项目的实战后,我发现只要掌握了几个基础外设的使用方法,就能完成80%的嵌入式开发需求。这篇文章将分享我在STM32基础外设开发中的实战经验,特别适合刚入门嵌入式开发的工程师和学生。
STM32作为目前最流行的ARM Cortex-M系列微控制器,广泛应用于工业控制、消费电子、物联网等领域。它的强大之处在于丰富的外设资源——GPIO、USART、SPI、I2C、ADC、定时器等,这些外设就像乐高积木,通过合理组合可以构建各种功能。下面我将从最基础的GPIO开始,逐步介绍每个外设的使用要点和常见问题。
2. 硬件准备与环境搭建
2.1 开发板选型建议
对于初学者,我推荐使用STM32F103C8T6最小系统板(俗称"蓝莓板")。这款开发板价格低廉(约20元),但包含了所有基础外设,且社区资源丰富。更高级的F4或H7系列虽然性能更强,但对初学者来说配置更复杂。
注意:购买开发板时一定要确认是否带有ST-Link调试器,如果没有需要额外购买。调试器是开发过程中必不可少的工具。
2.2 开发环境配置
我习惯使用Keil MDK作为开发环境,虽然它需要付费授权,但提供了完善的调试功能和丰富的示例代码。对于预算有限的开发者,也可以选择免费的STM32CubeIDE,这是ST官方推出的集成开发环境。
安装步骤:
- 下载并安装Keil MDK或STM32CubeIDE
- 安装对应芯片系列的Device Family Pack(DFP)
- 连接开发板,安装USB驱动
- 创建一个新工程,选择正确的芯片型号
常见问题:如果遇到"No ST-Link detected"错误,通常是因为驱动未正确安装。可以尝试重新插拔USB线或更新ST-Link驱动。
3. GPIO外设详解与应用
3.1 GPIO基本概念
GPIO(General Purpose Input/Output)是STM32最基础也是最重要的外设。每个GPIO引脚都可以独立配置为输入或输出模式,并且有多种工作方式:
- 输入模式:浮空输入、上拉输入、下拉输入
- 输出模式:推挽输出、开漏输出
- 复用功能:用于连接其他外设(如USART、SPI等)
- 模拟模式:用于ADC或DAC
3.2 GPIO配置实战
下面是一个典型的GPIO初始化代码(以STM32 HAL库为例):
c复制// 初始化LED引脚(PC13)
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOC_CLK_ENABLE(); // 使能GPIOC时钟
GPIO_InitStruct.Pin = GPIO_PIN_13;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 无上拉下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
// 控制LED
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // 点亮
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // 熄灭
经验分享:在操作GPIO前,一定要记得先使能对应的GPIO时钟(__HAL_RCC_GPIOx_CLK_ENABLE())。这是新手最容易忽略的一点,会导致GPIO无法正常工作。
3.3 GPIO中断应用
GPIO中断在按键检测、外部事件触发等场景非常有用。配置步骤如下:
- 配置GPIO为输入模式,并启用中断
- 配置NVIC(嵌套向量中断控制器)
- 编写中断服务函数
c复制// 按键中断配置(PA0)
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING; // 上升沿触发中断
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置NVIC
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
// 中断服务函数
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) {
// 处理按键事件
}
}
4. USART串口通信实战
4.1 USART基础配置
USART(通用同步异步收发器)是STM32与外部设备通信的重要接口,常用于调试输出、与PC通信等场景。配置USART需要关注以下几个参数:
- 波特率:常见的有9600、115200等
- 数据位:通常为8位
- 停止位:1位或2位
- 校验位:无校验、奇校验或偶校验
c复制// USART1初始化(PA9-TX, PA10-RX)
UART_HandleTypeDef huart1;
void USART1_Init(void) {
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
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);
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);
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
4.2 串口收发实现
STM32 HAL库提供了多种串口通信方式:
- 轮询方式:简单但效率低
- 中断方式:适合不定长数据接收
- DMA方式:高效,适合大数据量传输
c复制// 发送数据(轮询方式)
char msg[] = "Hello STM32!\r\n";
HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
// 接收数据(中断方式)
uint8_t rx_buffer[64];
HAL_UART_Receive_IT(&huart1, rx_buffer, 1); // 每次接收1字节
// 接收完成回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if(huart->Instance == USART1) {
// 处理接收到的数据
HAL_UART_Receive_IT(&huart1, rx_buffer, 1); // 重新启用接收
}
}
调试技巧:在开发初期,建议使用printf重定向到串口输出调试信息。这可以通过重写_write函数实现:
c复制int _write(int file, char *ptr, int len) {
HAL_UART_Transmit(&huart1, (uint8_t*)ptr, len, HAL_MAX_DELAY);
return len;
}
5. 定时器应用详解
5.1 定时器基本概念
STM32的定时器功能非常强大,可以实现:
- 基本定时:精确延时
- PWM输出:控制电机、LED亮度等
- 输入捕获:测量脉冲宽度
- 编码器接口:读取旋转编码器
STM32F103C8T6有3个通用定时器(TIM2、TIM3、TIM4)和1个高级定时器(TIM1)。
5.2 定时器延时实现
c复制// 定时器2初始化(1ms中断)
TIM_HandleTypeDef htim2;
void TIM2_Init(void) {
__HAL_RCC_TIM2_CLK_ENABLE();
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, 0, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
// 定时器中断服务函数
void TIM2_IRQHandler(void) {
HAL_TIM_IRQHandler(&htim2);
}
// 定时器中断回调函数
volatile uint32_t timer_count = 0;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim->Instance == TIM2) {
timer_count++;
}
}
// 毫秒延时函数
void delay_ms(uint32_t ms) {
uint32_t start = timer_count;
while((timer_count - start) < ms);
}
5.3 PWM输出配置
PWM(脉宽调制)在控制电机速度、LED亮度等方面应用广泛。下面是如何配置TIM3的通道1(PA6)输出PWM:
c复制TIM_HandleTypeDef htim3;
TIM_OC_InitTypeDef sConfigOC = {0};
void PWM_Init(void) {
__HAL_RCC_TIM3_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// GPIO配置
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF2_TIM3;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 定时器基础配置
htim3.Instance = TIM3;
htim3.Init.Prescaler = 720 - 1; // 72MHz/720 = 100kHz
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 100 - 1; // PWM频率 = 100kHz/100 = 1kHz
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&htim3);
// PWM通道配置
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 50; // 初始占空比50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}
// 改变PWM占空比
void set_pwm_duty(uint8_t duty) {
if(duty > 100) duty = 100;
sConfigOC.Pulse = duty;
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}
6. ADC模数转换应用
6.1 ADC基础配置
ADC(模数转换器)用于将模拟信号(如温度、电压)转换为数字值。STM32F103C8T6有一个12位ADC,最大采样率1MHz。
c复制ADC_HandleTypeDef hadc1;
void ADC1_Init(void) {
__HAL_RCC_ADC1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置ADC输入引脚(PA1)
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// ADC配置
hadc1.Instance = ADC1;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.NbrOfConversion = 1;
HAL_ADC_Init(&hadc1);
// 配置ADC通道
ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}
// 读取ADC值
uint16_t read_adc(void) {
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
return HAL_ADC_GetValue(&hadc1);
}
6.2 ADC使用技巧
-
参考电压:STM32F103的VDDA通常接3.3V,这是ADC的参考电压。如果需要精确测量,建议使用外部精密参考电压源。
-
采样时间:对于高阻抗信号源,需要增加采样时间(ADC_SAMPLETIME)以获得准确结果。
-
多通道采样:可以通过配置ScanConvMode和DiscontinuousConvMode实现多通道轮流采样。
-
DMA传输:对于高速采样,建议使用DMA将ADC数据直接传输到内存,避免CPU干预。
c复制// 多通道ADC DMA采样示例
uint16_t adc_values[2]; // 存储两个通道的结果
void ADC1_DMA_Init(void) {
// ...初始化代码与前面类似...
hadc1.Init.ScanConvMode = ENABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.NbrOfConversion = 2;
// 配置通道1和通道2
ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
sConfig.Channel = ADC_CHANNEL_2;
sConfig.Rank = 2;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
// 启动DMA传输
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_values, 2);
}
7. SPI和I2C通信接口
7.1 SPI接口配置
SPI(串行外设接口)是一种高速全双工通信协议,常用于连接Flash、显示屏、传感器等设备。
c复制SPI_HandleTypeDef hspi1;
void SPI1_Init(void) {
__HAL_RCC_SPI1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// SPI引脚配置(PA5-SCK, PA6-MISO, PA7-MOSI)
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// SPI配置
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_32;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 10;
HAL_SPI_Init(&hspi1);
}
// SPI发送接收数据
uint8_t spi_transfer(uint8_t data) {
uint8_t rx_data;
HAL_SPI_TransmitReceive(&hspi1, &data, &rx_data, 1, HAL_MAX_DELAY);
return rx_data;
}
7.2 I2C接口配置
I2C(Inter-Integrated Circuit)是一种两线制串行总线,适合连接多个低速设备。
c复制I2C_HandleTypeDef hi2c1;
void I2C1_Init(void) {
__HAL_RCC_I2C1_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
// I2C引脚配置(PB6-SCL, PB7-SDA)
GPIO_InitTypeDef GPIO_InitStruct = {0};
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;
GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// I2C配置
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读写函数
HAL_StatusTypeDef i2c_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t data) {
return HAL_I2C_Mem_Write(&hi2c1, dev_addr<<1, reg_addr,
I2C_MEMADD_SIZE_8BIT, &data, 1, HAL_MAX_DELAY);
}
HAL_StatusTypeDef i2c_read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data) {
return HAL_I2C_Mem_Read(&hi2c1, dev_addr<<1, reg_addr,
I2C_MEMADD_SIZE_8BIT, data, 1, HAL_MAX_DELAY);
}
调试技巧:I2C通信失败时,可以使用逻辑分析仪或示波器检查SCL和SDA信号。常见问题包括上拉电阻不合适(通常4.7kΩ)、地址错误(7位地址需要左移1位)或总线冲突。
8. 常见问题与解决方案
8.1 外设无法正常工作
现象:配置了外设但没有任何反应。
排查步骤:
- 检查时钟是否使能(__HAL_RCC_xxx_CLK_ENABLE())
- 检查引脚配置是否正确(特别是复用功能AF编号)
- 使用调试器检查寄存器值是否符合预期
- 检查硬件连接(电源、地线、信号线)
8.2 中断不触发
现象:配置了中断但从未进入中断服务函数。
排查步骤:
- 确认NVIC已正确配置(优先级和使能)
- 检查中断标志是否被清除
- 确认中断触发条件是否满足(如GPIO电平变化)
- 在启动文件中确认中断向量表正确
8.3 通信不稳定
现象:SPI/I2C/USART偶尔出现数据错误。
解决方案:
- 检查波特率/时钟频率是否与设备匹配
- 增加适当的延时(特别是对响应慢的设备)
- 检查信号质量(是否受到干扰,是否需要上拉电阻)
- 对于长距离通信,考虑使用RS485等更可靠的协议
8.4 功耗过高
现象:电池供电时设备续航时间短。
优化方法:
- 不使用的外设关闭时钟(__HAL_RCC_xxx_CLK_DISABLE())
- 进入低功耗模式(STOP或STANDBY模式)
- 降低主频(通过修改时钟树配置)
- 未使用的IO设置为模拟输入(最低功耗状态)
9. 项目实战:综合应用示例
9.1 环境监测系统设计
结合前面介绍的外设,我们可以构建一个简单的环境监测系统,功能包括:
- 通过ADC读取温度传感器(如NTC热敏电阻)
- 通过I2C读取湿度传感器(如SHT30)
- 通过USART将数据发送到上位机
- 通过PWM控制风扇速度
- 通过GPIO控制状态LED
c复制// 主循环示例
while(1) {
// 读取温度
float temperature = read_temperature();
// 读取湿度
float humidity = read_humidity();
// 根据温度控制风扇
if(temperature > 30.0f) {
set_pwm_duty(80); // 高温时全速运行
} else {
set_pwm_duty(20); // 低温时低速运行
}
// 发送数据到上位机
printf("Temp: %.1fC, Humidity: %.1f%%\r\n", temperature, humidity);
// 闪烁LED指示系统运行
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
// 延时1秒
HAL_Delay(1000);
}
9.2 性能优化技巧
-
减少HAL_Delay使用:在实时性要求高的应用中,避免使用阻塞式延时,改用定时器中断或状态机。
-
合理使用DMA:对于大量数据传输(如ADC采样、USART通信),使用DMA可以显著降低CPU负载。
-
中断优先级管理:合理设置中断优先级(NVIC),确保关键任务能够及时响应。
-
电源管理:在电池供电应用中,充分利用STM32的低功耗模式,如SLEEP、STOP和STANDBY模式。
-
代码优化:对性能关键代码,可以考虑直接操作寄存器代替HAL库函数,或使用编译器优化选项(-O2或-O3)。
10. 进阶学习建议
掌握了基础外设后,可以进一步学习以下内容:
-
RTOS应用:FreeRTOS或RT-Thread等实时操作系统可以更好地管理复杂任务。
-
硬件加速:学习使用STM32的硬件CRC、DMA2D等加速单元。
-
高级定时器:探索STM32高级定时器的死区控制、互补输出等高级功能。
-
低功耗设计:深入理解STM32的低功耗模式和使用场景。
-
固件升级:学习IAP(在应用编程)实现远程固件更新。
-
调试技巧:掌握更高级的调试方法,如SWO输出、断点设置、变量实时监控等。
在实际项目中,我建议从简单的功能开始,逐步增加复杂度。每次添加新功能时,都先单独测试确保工作正常,再集成到主系统中。这种模块化开发方式可以大大减少调试难度。
