Skip to content

外设驱动开发

MCU 的价值在于它集成的外设:GPIO 控制 LED 和按键、UART 做串口通信、SPI/I2C 连接传感器、ADC 采集模拟信号、PWM 驱动电机、DMA 搬运数据。掌握外设驱动是嵌入式开发的核心技能。

本篇以 STM32F4 为例,每个外设给出寄存器级和 HAL 库两种写法。

一、GPIO

GPIO(General Purpose Input/Output)是最基础的外设,控制引脚的高低电平。

输出模式(点亮 LED)

寄存器级

c
// 开启 GPIOA 时钟
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

// PA5 配置为推挽输出
GPIOA->MODER &= ~(3 << 10);   // 清除 bit[11:10]
GPIOA->MODER |= (1 << 10);    // 01 = 输出模式
GPIOA->OTYPER &= ~(1 << 5);   // 0 = 推挽
GPIOA->OSPEEDR |= (2 << 10);  // 10 = 高速

// 输出高电平
GPIOA->BSRR = (1 << 5);       // 置位(原子操作)
// 输出低电平
GPIOA->BSRR = (1 << 21);      // 复位(写高 16 位)

HAL 库

c
GPIO_InitTypeDef gpio = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
gpio.Pin = GPIO_PIN_5;
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &gpio);

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);

输入模式(读按键)

c
// PA0 配置为输入,内部上拉
GPIOA->MODER &= ~(3 << 0);    // 00 = 输入
GPIOA->PUPDR &= ~(3 << 0);
GPIOA->PUPDR |= (1 << 0);     // 01 = 上拉

// 读取电平
if (!(GPIOA->IDR & (1 << 0))) {
    // 按键按下(低电平有效)
}

外部中断(EXTI)

按键用轮询会浪费 CPU,用外部中断更合理:

c
// HAL 方式配置 PA0 下降沿中断
gpio.Pin = GPIO_PIN_0;
gpio.Mode = GPIO_MODE_IT_FALLING;
gpio.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &gpio);

HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

// 中断回调
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == GPIO_PIN_0) {
        // 按键处理(注意消抖)
    }
}

二、UART

UART 是嵌入式最常用的通信接口,调试打印、AT 指令、传感器数据都靠它。

寄存器级发送

c
// 开时钟、配置引脚(AF7)、配置波特率
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
// ... 引脚配置省略

// 波特率 = fck / (16 * USARTDIV)
// 84MHz / (16 * 115200) ≈ 45.5625 → BRR = (45 << 4) | 9
USART1->BRR = 0x02D9;
USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;

// 发送一个字节
void uart_send(uint8_t ch) {
    while (!(USART1->SR & USART_SR_TXE));
    USART1->DR = ch;
}

HAL + 中断接收

c
UART_HandleTypeDef huart1;
uint8_t rx_byte;

// 初始化
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;
HAL_UART_Init(&huart1);

// 开启接收中断
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);

// 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    ring_buffer_push(&rx_buf, rx_byte);
    HAL_UART_Receive_IT(huart, &rx_byte, 1);  // 重新开启
}

printf 重定向

c
int _write(int fd, char *ptr, int len) {
    HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
    return len;
}

三、SPI

SPI 速度快、全双工,常用于 Flash、显示屏、高速 ADC。

c
// HAL 方式初始化
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;   // CPOL=0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;       // CPHA=0
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;
HAL_SPI_Init(&hspi1);

// 读写 SPI Flash(W25Q128)
void flash_read_id(uint8_t *id) {
    uint8_t cmd = 0x9F;  // JEDEC ID
    HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET);
    HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
    HAL_SPI_Receive(&hspi1, id, 3, 100);
    HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET);
}

四、I2C

I2C 两根线挂多个设备,适合低速传感器。

c
// 读取 BME280 温湿度传感器
uint8_t reg = 0xF7;  // 数据寄存器起始地址
uint8_t data[8];

HAL_I2C_Master_Transmit(&hi2c1, 0x76 << 1, &reg, 1, 100);
HAL_I2C_Master_Receive(&hi2c1, 0x76 << 1, data, 8, 100);

// 或用 Mem 系列函数(更简洁)
HAL_I2C_Mem_Read(&hi2c1, 0x76 << 1, 0xF7, I2C_MEMADD_SIZE_8BIT, data, 8, 100);

I2C 常见问题排查:

  • 总线挂死 → 检查上拉电阻、尝试发 9 个时钟脉冲恢复
  • NAK → 地址错误(注意 7 位地址要左移 1 位)
  • 速度不对 → 检查时钟配置和上拉电阻值

五、ADC

ADC 把模拟电压转换为数字值。STM32F4 的 ADC 是 12 位,量程 0~3.3V。

c
// 单次转换
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 100);
uint32_t raw = HAL_ADC_GetValue(&hadc1);

// 转换为电压
float voltage = raw * 3.3f / 4095.0f;

// 转换为温度(NTC 热敏电阻分压电路)
float resistance = 10000.0f * raw / (4095.0f - raw);
float temperature = 1.0f / (log(resistance / 10000.0f) / 3950.0f + 1.0f / 298.15f) - 273.15f;

多通道采集用 DMA 最高效,避免 CPU 轮询等待。

六、PWM 与定时器

PWM(脉宽调制)通过改变占空比控制平均输出电压,用于 LED 调光、电机调速、舵机控制。

c
// TIM3 CH1 输出 1kHz PWM,占空比 50%
// 假设 TIM3 时钟 84MHz,预分频 83 → 计数频率 1MHz
// ARR = 999 → PWM 频率 = 1MHz / 1000 = 1kHz
htim3.Instance = TIM3;
htim3.Init.Prescaler = 83;
htim3.Init.Period = 999;
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);

// 设置占空比
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500);  // 50%
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 250);  // 25%

舵机控制(50Hz PWM,脉宽 0.5~2.5ms 对应 0~180°):

c
// ARR = 19999, PSC = 83 → 50Hz
// 0° = 500, 90° = 1500, 180° = 2500
void servo_set_angle(uint16_t angle) {
    uint16_t pulse = 500 + angle * 2000 / 180;
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse);
}

七、DMA

DMA(Direct Memory Access)让外设和内存之间直接搬运数据,不需要 CPU 介入。适合大量数据传输(ADC 多通道采集、UART 收发大包、SPI Flash 读写)。

c
// UART DMA 发送
uint8_t tx_buf[] = "Hello DMA\r\n";
HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf) - 1);
// CPU 可以去做其他事,传输完成会触发回调

// ADC + DMA 连续采集
uint16_t adc_buf[4];  // 4 通道
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 4);
// adc_buf 会被 DMA 自动填充,无需 CPU 干预

DMA 注意事项:

  • 缓冲区不能是局部变量(DMA 传输期间函数可能已返回)
  • 双缓冲(Double Buffer)模式适合音频等连续流
  • 注意 Cache 一致性(Cortex-M7 有 D-Cache,DMA 前要 Clean/Invalidate)

八、看门狗

看门狗定时器防止程序跑飞。如果程序在规定时间内没有"喂狗",看门狗会触发系统复位。

c
// 独立看门狗(IWDG),LSI 时钟驱动,不依赖主时钟
hiwdg.Instance = IWDG;
hiwdg.Init.Prescaler = IWDG_PRESCALER_64;
hiwdg.Init.Reload = 625;  // 超时 ≈ 1 秒(32kHz / 64 / 625)
HAL_IWDG_Init(&hiwdg);

// 主循环中定期喂狗
while (1) {
    HAL_IWDG_Refresh(&hiwdg);
    // ... 业务逻辑
}

窗口看门狗(WWDG)更严格:必须在特定时间窗口内喂狗,太早或太晚都会复位。

九、外设选型与组合

需求推荐外设组合
调试打印UART + printf 重定向
读温湿度传感器I2C(BME280)或 1-Wire(DS18B20)
读加速度/陀螺仪SPI(MPU6500)或 I2C(MPU6050)
驱动 TFT 显示屏SPI(ST7789)+ DMA
电机调速PWM + 定时器
多通道电压采集ADC + DMA
大数据量传输UART/SPI + DMA
存储数据到 FlashSPI(W25Q128)
读 SD 卡SDIO 或 SPI 模式

返回 总览与学习路线

别急,先让缓存热一下。