核心外设驱动开发(上):GPIO与LED指示灯控制、SysTick系统定时器、UART串口通信与printf重定向、中断优先级配置与NVIC管理
好,咱们直接进入正题。这一章讲的是嵌入式开发里最基础、也最绕不开的几个外设模块。你想想看,不管你是做充电桩、智能家居还是工业控制器,GPIO、定时器、串口、中断——这四样东西几乎每个项目都会用到。我个人习惯把这四个模块称为「嵌入式开发的四根柱子」,柱子稳了,上面的应用层才能盖得高。
GPIO与LED指示灯控制
GPIO,说白了就是芯片的「手脚」。你可以让它输出高电平去点亮一个LED,也可以读取它的电平来判断按键有没有被按下。在充电桩项目里,LED指示灯用来显示状态:绿色代表空闲,蓝色代表充电中,红色代表故障。
嗯,这里要注意一个坑。我在项目中遇到过好几次,新手工程师配置GPIO时只设置了模式,忘了配置上下拉电阻。结果LED在应该熄灭的时候微微发亮,查了半天才发现是引脚浮空导致的。
GPIO配置的核心步骤:
- 使能GPIO外设时钟(RCC_APB2PeriphClockCmd)
- 配置GPIO模式(输入/输出/复用/模拟)
- 配置输出类型(推挽/开漏)
- 配置速度(2MHz/10MHz/50MHz)
- 配置上下拉电阻(上拉/下拉/浮空)
来看一个实际的代码示例。这是我在充电桩项目里用的LED驱动初始化函数:
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置PB0、PB1、PB2为推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 初始状态全部熄灭
GPIO_SetBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2);
}
你可能会问,为什么速度要选50MHz?其实对于LED这种低频应用,2MHz就足够了。但我习惯统一用50MHz,这样以后复用成其他功能时不用再改配置。当然,如果你对功耗有严格要求,那就按需设置。
小技巧:用位带操作来单独控制某个引脚,比用GPIO_SetBits/GPIO_ResetBits要快得多。我在做充电桩的呼吸灯效果时,就是用位带操作在中断里快速翻转引脚,实现了平滑的PWM效果。
SysTick系统定时器
SysTick是Cortex-M内核自带的24位递减计数器。它最大的好处是——所有Cortex-M芯片都有,代码可以无缝移植。我建议你把它当作系统的「心跳」,用来产生1ms的时基。
为什么是1ms?因为大多数实时任务的时间粒度都在毫秒级。比如充电桩里检测充电枪连接状态,每10ms扫描一次就够了;而LED闪烁,500ms翻转一次。
配置SysTick其实很简单,核心就三步:
- 设置重装载值(决定中断频率)
- 选择时钟源(AHB/8 或 AHB)
- 使能中断并启动计数器
我记得第一次做充电桩项目时,SysTick配置错了时钟源。我选了AHB时钟源,结果72MHz的时钟让SysTick每1.39微秒就中断一次,系统直接卡死。后来查了手册才发现,默认是AHB/8,也就是9MHz。
// SysTick配置函数,产生1ms中断
void SysTick_Init(void)
{
// 系统时钟72MHz,AHB/8 = 9MHz
// 重装载值 = 9000000 / 1000 = 9000
if (SysTick_Config(SystemCoreClock / 1000))
{
// 配置失败,进入死循环
while (1);
}
}
// SysTick中断服务函数
void SysTick_Handler(void)
{
static uint32_t tick = 0;
tick++;
// 每500ms翻转一次LED
if (tick % 500 == 0)
{
GPIO_ToggleBits(GPIOB, GPIO_Pin_0);
}
}
注意:SysTick_Config函数会默认使能中断。如果你只想用轮询方式读取计数,记得在配置后手动关闭中断:SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk。
UART串口通信与printf重定向
串口是嵌入式开发者的「眼睛」。没有串口打印,调试就像蒙着眼睛走路。在充电桩项目里,串口用来输出调试信息、与上位机通信、甚至升级固件。
printf重定向,说白了就是把printf输出的字符串,通过串口发送出去。这样你就可以在代码里用printf("充电电流: %d mA", current)来打印变量,比用USART_SendData一个一个发方便太多了。
实现printf重定向,需要做两件事:
- 配置好UART(波特率、数据位、停止位、校验位)
- 重写fputc函数,让它调用UART发送一个字节
来看代码:
// UART初始化
void UART_Init(void)
{
USART_InitTypeDef USART_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
// 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 配置TX(PA9)为复用推挽输出
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);
// 配置RX(PA10)为浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置UART参数
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);
USART_Cmd(USART1, ENABLE);
}
// printf重定向
int fputc(int ch, FILE *f)
{
// 等待发送缓冲区空
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, (uint8_t)ch);
return ch;
}
避坑指南:我曾经在printf重定向后,发现程序一运行就卡死。查了半天,发现是MDK工程里没有勾选「Use MicroLIB」。MicroLIB提供了精简版的printf实现,不勾选的话,fputc不会被调用。另外,如果你用GCC编译器,记得在链接脚本里加上--specs=nano.specs。
中断优先级配置与NVIC管理
中断优先级,说白了就是决定「谁先被服务」。在充电桩项目里,中断优先级配置不好,会导致严重的问题。比如,充电过流保护中断的优先级如果低于某个普通外设中断,那过流发生时可能无法及时响应,后果不堪设想。
NVIC(嵌套向量中断控制器)支持两种优先级分组:抢占优先级和子优先级。抢占优先级高的中断可以打断优先级低的中断,形成嵌套。子优先级则用于同一抢占优先级下的中断排队。
| 优先级分组 | 抢占优先级位数 | 子优先级位数 | 应用场景 |
|---|---|---|---|
| NVIC_PriorityGroup_0 | 0位 | 4位 | 所有中断同级,无嵌套 |
| NVIC_PriorityGroup_1 | 1位 | 3位 | 2级抢占,8级子优先级 |
| NVIC_PriorityGroup_2 | 2位 | 2位 | 4级抢占,4级子优先级 |
| NVIC_PriorityGroup_3 | 3位 | 1位 | 8级抢占,2级子优先级 |
| NVIC_PriorityGroup_4 | 4位 | 0位 | 16级抢占,无子优先级 |
我个人习惯用NVIC_PriorityGroup_2,也就是4级抢占优先级、4级子优先级。这样既保证了关键中断(比如过流保护)可以打断普通中断,又给同一优先级的中断留了排队空间。
配置中断优先级时,一定要在系统初始化时就设置好分组,中途不要更改。为什么?因为NVIC的分组设置是全局的,一旦更改,所有已配置的中断优先级含义都会变化,容易引发不可预知的行为。
// 中断优先级配置示例
void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
// 设置优先级分组为4级抢占,4级子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 配置USART1中断
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 配置SysTick中断(抢占优先级最低)
NVIC_InitStructure.NVIC_IRQChannel = SysTick_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; // 抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; // 子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
重要提醒:中断服务函数要短小精悍,不要在中断里做复杂运算或调用printf。我在项目中遇到过,有人在UART中断里直接调用printf,结果printf又触发UART中断,形成了递归调用,栈直接爆了。正确的做法是:中断里只做标志位设置或数据接收,具体处理放到主循环里做。
好了,这一章的内容就到这里。GPIO、SysTick、UART、NVIC——这四个模块是嵌入式开发的基石。你把这些吃透了,后面做充电桩的通信协议、状态机、故障处理就会轻松很多。下一章我们讲定时器、PWM和ADC,这些是控制充电功率的关键。