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秒中断一次
- 中断里启动ADC转换
- 转换完成后读取数据
- 通过串口打印出来
代码框架:
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模块的驱动,那才是真正的好戏。