第三章 嵌入式系统基础:STM32/ESP32平台介绍、GPIO、定时器、中断、PWM基础

各位同学,欢迎来到第三章。这一章我们聊聊嵌入式平台的基础。

做智能药盒,说白了就是让一块小芯片去控制几个电机、读几个传感器、再驱动个屏幕。听起来简单,但里面的门道不少。我个人习惯把嵌入式开发比作「搭积木」——你得先认识每块积木长什么样,才能搭出漂亮的房子。

今天我们就来认识两块最常见的积木:STM32ESP32

3.1 两大主流平台:STM32 vs ESP32

先说说STM32。这是意法半导体的产品,ARM Cortex-M内核。我在项目中用过不下十款STM32芯片,从F103到H743都有。它的特点是稳定、功耗低、外设丰富。做药盒这种需要长期运行的设备,STM32是首选。

ESP32呢?乐鑫的产品,集成了Wi-Fi和蓝牙。如果你想让药盒联网、远程提醒、数据上传,ESP32就很合适。不过它的功耗比STM32高一些,待机时要注意。

特性 STM32(以F103为例) ESP32
内核 ARM Cortex-M3 Xtensa LX6 双核
主频 72 MHz 240 MHz
无线 无(需外接) Wi-Fi + BLE
功耗 低(uA级待机) 较高(mA级待机)
开发工具 Keil / STM32CubeIDE Arduino / ESP-IDF
适合场景 控制类、低功耗设备 联网类、数据处理
我的建议:如果药盒不需要联网,用STM32。如果一定要联网,可以考虑双芯片方案——STM32做控制,ESP32做通信,两者通过UART或SPI通信。我在一个血糖仪项目中就这么干过,效果不错。

3.2 GPIO:最基础的操作

GPIO,全称通用输入输出口。说白了就是芯片的「手脚」——可以输出高电平或低电平,也可以读取外部信号的高低。

在药盒里,GPIO用来做什么?

  • 控制LED指示灯(吃药提醒灯)
  • 读取按键状态(用户按了哪个按钮)
  • 驱动蜂鸣器(闹钟响)
  • 控制电机驱动芯片的使能脚

嗯,这里要注意:GPIO不能直接驱动大电流设备。比如一个直流电机,启动电流可能几百毫安,GPIO只能输出几毫安。强行驱动会烧坏芯片。我曾经有个实习生就这么干过,芯片直接冒烟了……

避坑指南:GPIO驱动负载时,电流不要超过芯片手册规定的最大值(通常20mA以内)。驱动电机、继电器等,必须加驱动芯片或三极管。

看个简单的代码示例,STM32上用HAL库点亮一个LED:

// 初始化GPIO
void LED_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    __HAL_RCC_GPIOB_CLK_ENABLE();  // 使能GPIOB时钟
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

// 点亮LED
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);

ESP32上用Arduino框架更简单:

void setup() {
    pinMode(2, OUTPUT);  // 板载LED通常接GPIO2
}

void loop() {
    digitalWrite(2, HIGH);
    delay(1000);
    digitalWrite(2, LOW);
    delay(1000);
}

3.3 定时器:精确的时间管理

定时器是嵌入式系统里最常用的外设之一。你想想看,药盒需要定时提醒吃药、定时检测药仓状态、定时上报数据……这些都离不开定时器。

STM32的定时器分几种:基本定时器、通用定时器、高级定时器。做药盒的话,通用定时器就够用了。

定时器的核心原理:内部有一个计数器,每个时钟周期加1。当计数值达到预设的「自动重装载值」时,就会触发中断或产生事件。

关键公式:定时时间 = (预分频值 + 1) × (自动重装载值 + 1) / 定时器时钟频率

举个例子,STM32F103的定时器时钟是72MHz,我想产生1秒的中断:

  • 预分频值设为7199(即7200分频,得到10kHz)
  • 自动重装载值设为9999(即10000个计数)
  • 定时时间 = 7200 × 10000 / 72,000,000 = 1秒

代码实现:

void TIM2_Init(void) {
    TIM_HandleTypeDef htim2;
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 7199;      // 预分频
    htim2.Init.Period = 9999;         // 自动重装载值
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    HAL_TIM_Base_Init(&htim2);
    HAL_TIM_Base_Start_IT(&htim2);    // 启动定时器并开启中断
}

// 中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM2) {
        // 每秒执行一次
        Check_Medicine_Time();  // 检查吃药时间
    }
}
经验之谈:我在做药盒时,用了三个定时器:一个做秒计时(提醒),一个做电机PWM(后面会讲),一个做按键消抖延时。合理分配定时器资源很重要。

3.4 中断:让系统及时响应

中断是什么?打个比方:你正在看书,突然电话响了。你放下书去接电话,接完回来继续看书。这个「电话响」就是中断信号,「接电话」就是中断服务函数。

在嵌入式系统里,中断让芯片能及时响应外部事件。比如:

  • 按键按下 → 触发外部中断 → 执行吃药确认
  • 定时器溢出 → 触发定时器中断 → 更新显示
  • UART收到数据 → 触发串口中断 → 解析指令

STM32的中断优先级可以配置。我个人习惯把时间敏感的中断设高优先级(比如电机堵转检测),把不那么紧急的设低优先级(比如按键扫描)。

注意:中断服务函数里不要做耗时操作!比如不要在里面调用HAL_Delay(),不要做复杂的计算。我曾经见过有人把printf写在中断里,结果系统卡死——因为printf太慢了。正确的做法是:中断里只设置标志位,在主循环里处理。

外部中断的配置示例:

void EXTI0_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    __HAL_RCC_GPIOA_CLK_ENABLE();
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;  // 下降沿触发
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 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) {
        key_pressed = 1;  // 设置标志位,主循环处理
    }
}

3.5 PWM:模拟世界的数字控制

PWM,脉冲宽度调制。说白了就是通过快速开关信号,让负载感受到一个「平均电压」。

在药盒里,PWM的典型应用:

  • 控制直流电机转速(出药机构)
  • 调节LED亮度(夜间模式)
  • 驱动蜂鸣器发出不同音调

PWM有两个关键参数:频率和占空比。频率决定了开关的快慢,占空比决定了平均电压的高低。

参数 说明 药盒中的典型值
频率 每秒开关次数 电机:1kHz~10kHz;LED:100Hz以上
占空比 高电平时间占整个周期的比例 0%~100%,控制电机速度或LED亮度

STM32用定时器产生PWM非常方便。以TIM3的通道1为例:

void PWM_Init(void) {
    TIM_HandleTypeDef htim3;
    TIM_OC_InitTypeDef sConfigOC = {0};
    
    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 71;       // 72MHz / 72 = 1MHz
    htim3.Init.Period = 999;         // PWM频率 = 1MHz / 1000 = 1kHz
    htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
    HAL_TIM_PWM_Init(&htim3);
    
    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 500;           // 初始占空比50%
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
    
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}

// 动态调整占空比
void Set_Motor_Speed(uint16_t speed) {
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, speed);
    // speed范围0~1000,对应0%~100%占空比
}
避坑指南:电机PWM的频率不能太低,否则电机会发出刺耳的噪音(人耳能听到20Hz~20kHz)。我一般用1kHz以上。另外,启动电机时不要直接给100%占空比,应该软启动——从0逐渐增加到目标值,这样能减少冲击电流。

ESP32的PWM实现更灵活,因为它有LEDC控制器:

// ESP32 Arduino PWM
const int freq = 5000;      // 5kHz
const int ledChannel = 0;
const int resolution = 8;   // 8位分辨率,0~255

void setup() {
    ledcSetup(ledChannel, freq, resolution);
    ledcAttachPin(2, ledChannel);  // GPIO2输出PWM
}

void loop() {
    for (int dutyCycle = 0; dutyCycle <= 255; dutyCycle++) {
        ledcWrite(ledChannel, dutyCycle);
        delay(10);
    }
}

好了,这一章的内容就到这里。总结一下:我们认识了STM32和ESP32两个平台,掌握了GPIO的输入输出、定时器的精确计时、中断的及时响应、PWM的模拟控制。这些都是做智能药盒的「基本功」。

下一章,我们会把这些知识串起来,开始设计药盒的硬件电路。到时候见。