4. STM32基础编程:GPIO配置、定时器中断、串口通信(UART)、ADC采集

好,咱们进入第四讲。这一章是硬骨头,也是你后面做LoRa节点的基础中的基础。说白了,STM32就是整个节点的“大脑”,你得学会怎么让它动起来。

我个人习惯,拿到一个新项目,先搞定这四个模块:GPIO、定时器、串口、ADC。为什么?GPIO是手脚,定时器是心跳,串口是嘴巴,ADC是眼睛。这四个玩转了,剩下的就是搭积木。

4.1 GPIO配置:让芯片“开口说话”

GPIO,通用输入输出口。你想想看,控制一个LED亮灭,读取一个按键状态,靠的都是它。

在STM32里,GPIO不是随便就能用的。你得先告诉芯片:哪个引脚,干什么用,什么速度,什么模式。这就是配置。

核心要点:GPIO的八种模式里,最常用的是推挽输出(PP)和浮空输入(IN_FLOATING)。做输出用PP,做输入用IN_FLOATING或上拉输入。

我举个例子,控制一个LED。代码长这样:

// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

// 定义GPIO结构体
GPIO_InitTypeDef GPIO_InitStructure;

// 配置PB0为推挽输出,50MHz
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);

// 点亮LED
GPIO_SetBits(GPIOB, GPIO_Pin_0);

嗯,这里要注意:时钟使能这一步千万别忘。我刚开始学的时候,忘了开时钟,折腾了半天灯就是不亮。后来查手册才发现,STM32的外设默认时钟是关闭的,省电嘛。

我的小技巧:配置GPIO时,养成习惯先看原理图,确认引脚有没有复用功能。比如PA9和PA10,默认是GPIO,但同时也是USART1的TX和RX。你如果要用串口,就不能当普通GPIO用了。

4.2 定时器中断:精准的时间管理

定时器,说白了就是个计数器。给它一个时钟源,它就开始数数。数到预设值,就产生一个中断。这就是定时器中断。

为什么需要它?你想想看,你要每隔1秒采集一次传感器数据,总不能靠delay()死等吧?那CPU啥也别干了。用定时器中断,CPU该干嘛干嘛,时间到了自动跳进来处理。

配置代码示例:

// 使能TIM2时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

// 配置定时器
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 9999;        // 自动重装载值
TIM_TimeBaseStructure.TIM_Prescaler = 7199;     // 预分频器
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

// 使能中断
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
// 使能定时器
TIM_Cmd(TIM2, ENABLE);

这里有个计算:定时器频率 = 72MHz / (7199+1) / (9999+1) = 1Hz。也就是1秒中断一次。

我曾经踩过的坑:预分频器和自动重装载值的范围。TIM2是16位定时器,最大值65535。如果你要定时很长时间,比如1小时,就得用32位定时器(TIM5、TIM6)或者级联。别硬算,会溢出。

中断服务函数怎么写?

void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
        // 1秒到了,做你想做的事
        // 比如翻转LED
        GPIO_ToggleBits(GPIOB, GPIO_Pin_0);
        
        // 清除中断标志位
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}

注意:一定要清除中断标志位,否则会一直进中断,死循环。

4.3 串口通信(UART):和电脑“聊天”

串口,UART,是嵌入式开发最常用的调试手段。没有之一。你总得知道芯片在想什么吧?串口就是那个“传话筒”。

配置串口,其实和GPIO差不多,也是开时钟、配参数、使能。但多了一个东西:波特率。波特率就是通信速度,9600、115200最常见。

// 使能USART1和GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);

// 配置PA9为复用推挽输出,PA10为浮空输入
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);

// 配置USART1
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStructure);

// 使能USART1
USART_Cmd(USART1, ENABLE);

发送一个字符:

USART_SendData(USART1, 'A');
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); // 等待发送完成

我个人的习惯:写一个printf重定向函数,这样就能直接用printf打印调试信息了。方法就是重写fputc函数,把字符通过串口发出去。省事很多。

4.4 ADC采集:感知模拟世界

ADC,模数转换器。传感器输出的电压是模拟量,芯片只能处理数字量。ADC就是干这个的。

STM32的ADC是12位的,也就是0-4095。参考电压一般是3.3V,所以分辨率是3.3/4096 ≈ 0.8mV。

配置ADC,我建议用规则通道+软件触发的方式,简单可靠。

// 使能ADC1和GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);

// 配置PA0为模拟输入
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);

// 配置ADC
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);

// 配置ADC通道
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);

// 使能ADC
ADC_Cmd(ADC1, ENABLE);

// 校准
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));

采集一次数据:

// 启动转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
// 等待转换完成
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC));
// 读取结果
uint16_t adc_value = ADC_GetConversionValue(ADC1);

我曾经犯过的错:ADC采集结果跳动很大。后来发现是采样时间太短了。对于高阻抗的传感器,采样时间要设长一点,比如55.5个周期。另外,电源纹波也会影响精度,可以在ADC输入端加个0.1uF的电容。

4.5 综合实战:定时采集传感器数据并通过串口发送

好,现在我们把四个模块串起来。做一个实际的功能:每1秒采集一次ADC数据,然后通过串口发送到电脑。

思路很简单:

  1. 定时器1秒中断一次
  2. 中断里启动ADC转换
  3. 转换完成后读取数据
  4. 通过串口打印出来

代码框架:

volatile uint16_t adc_result = 0;
volatile uint8_t adc_ready = 0;

void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
        // 启动ADC转换
        ADC_SoftwareStartConvCmd(ADC1, ENABLE);
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}

// 在主循环中检查
while(1)
{
    if (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC))
    {
        adc_result = ADC_GetConversionValue(ADC1);
        // 计算实际电压值
        float voltage = (float)adc_result * 3.3f / 4096.0f;
        printf("ADC Value: %d, Voltage: %.2f V\r\n", adc_result, voltage);
        ADC_ClearFlag(ADC1, ADC_FLAG_EOC);
    }
}

核心总结:GPIO是基础,定时器是节奏,串口是沟通,ADC是感知。这四个模块,你练熟了,后面做LoRa节点就轻松了。记住:先配置时钟,再配置外设,最后使能。这个顺序别搞反。

嗯,这一章内容不少,但都是基本功。我建议你动手把每个模块单独跑一遍,然后再组合起来。代码写多了,自然就熟了。下一章,我们开始讲LoRa模块的驱动,那才是真正的好戏。