CH32 沁恒标准库实战指南:从GPIO到PWM的快速开发

张开发
2026/4/13 4:34:36 15 分钟阅读

分享文章

CH32 沁恒标准库实战指南:从GPIO到PWM的快速开发
1. 初识CH32标准库开发者的瑞士军刀第一次接触CH32系列单片机时我被官方提供的标准库彻底惊艳到了。这就像拿到了一套精密的乐高积木所有基础模块都已经预制好我们只需要按照需求组装就能快速搭建功能。标准库最厉害的地方在于它把底层寄存器操作封装成了直观的函数调用比如你想让PB0引脚输出高电平不用再翻手册查寄存器地址直接调用GPIO_SetBits(GPIOB, GPIO_Pin_0)就行。这里有个真实案例去年我做智能家居项目时需要快速验证GPIO控制继电器的方案。用标准库只花了15分钟就完成了电路板点亮测试而如果直接操作寄存器至少要多花半天时间研究芯片手册。标准库的GPIO_InitTypeDef结构体设计特别人性化初始化参数一目了然GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; // 选择0号引脚 GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出模式 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; // 输出速度有个新手容易踩的坑是忘记开启外设时钟。我就犯过这个错误调试半天发现GPIO没反应最后发现少写了RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE)。记住在CH32中任何外设使用前都必须先使能对应的时钟这就像给设备通电一样重要。2. GPIO实战从点灯到高级控制2.1 基础输出模式详解让我们用最经典的点灯大法入门。假设我们要让PB0引脚驱动LED首先需要理解GPIO的四种输出模式推挽输出(GPIO_Mode_Out_PP)能输出强高低电平最常用开漏输出(GPIO_Mode_Out_OD)只能拉低或高阻态需要上拉电阻复用推挽(GPIO_Mode_AF_PP)用于PWM等特殊功能复用开漏(GPIO_Mode_AF_OD)I2C等总线常用推挽模式配置示例void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStruct.GPIO_Pin GPIO_Pin_0; GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStruct); }实际项目中我更喜欢用位带操作实现快速IO控制。虽然标准库没有直接提供但我们可以用宏定义实现#define LED_ON() (GPIOB-BSHR GPIO_Pin_0) #define LED_OFF() (GPIOB-BCR GPIO_Pin_0) #define LED_TOGGLE() (GPIOB-OUTDR ^ GPIO_Pin_0)2.2 输入模式与中断实战GPIO输入模式在按键检测中特别重要。最近做的工业控制器项目里我遇到了按键消抖的问题。标准库结合定时器可以完美解决// 按键GPIO初始化 void KEY_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStruct.GPIO_Pin GPIO_Pin_0; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOA, GPIO_InitStruct); } // 在定时器中断中处理消抖 if(KEY_PRESSED()) { static uint8_t count 0; if(count 5) { // 连续检测到5次才认为有效 count 0; // 执行按键动作 } }中断配置是另一个重点。CH32的中断控制器设计得很灵活下面这个配置让PA0引脚下降沿触发中断void EXTI0_IRQHandler(void) __attribute__((interrupt(WCH-Interrupt-fast))); void KEY_EXTI_Init(void) { EXTI_InitTypeDef EXTI_InitStruct {0}; NVIC_InitTypeDef NVIC_InitStruct {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); EXTI_InitStruct.EXTI_Line EXTI_Line0; EXTI_InitStruct.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStruct.EXTI_Trigger EXTI_Trigger_Falling; EXTI_InitStruct.EXTI_LineCmd ENABLE; EXTI_Init(EXTI_InitStruct); NVIC_InitStruct.NVIC_IRQChannel EXTI0_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority 1; NVIC_InitStruct.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStruct); }3. 定时器的艺术从精准延时到PWM生成3.1 定时器基础配置定时器是单片机最强大的外设之一。在CH32中配置定时器需要理解几个关键参数Prescaler (PSC): 分频系数决定计数频率Counter Mode: 计数模式常用向上计数Period (ARR): 自动重装载值决定定时周期这个公式很重要定时时间 (ARR 1) × (PSC 1) / 时钟频率比如要配置1ms定时中断系统时钟96MHzvoid TIM3_Init(void) { TIM_TimeBaseInitTypeDef TIM_InitStruct {0}; NVIC_InitTypeDef NVIC_InitStruct {0}; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_InitStruct.TIM_Period 1000 - 1; // 自动重装载值 TIM_InitStruct.TIM_Prescaler 96 - 1; // 分频系数 TIM_InitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_InitStruct.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, TIM_InitStruct); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); NVIC_InitStruct.NVIC_IRQChannel TIM3_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority 1; NVIC_InitStruct.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStruct); TIM_Cmd(TIM3, ENABLE); }3.2 PWM高级应用技巧PWM在电机控制、LED调光中应用广泛。CH32的PWM配置有几个关键点定时器要配置为PWM模式需要设置占空比GPIO要配置为复用功能伺服电机控制示例周期20ms脉宽0.5-2.5msvoid PWM_Init(void) { TIM_OCInitTypeDef TIM_OCInitStruct {0}; TIM_TimeBaseInitTypeDef TIM_InitStruct {0}; // 时基配置 TIM_InitStruct.TIM_Period 20000 - 1; // 20ms周期 TIM_InitStruct.TIM_Prescaler 96 - 1; // 1MHz计数频率 TIM_InitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_InitStruct); // PWM模式配置 TIM_OCInitStruct.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStruct.TIM_Pulse 1500; // 初始占空比1.5ms TIM_OCInitStruct.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStruct); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM2, ENABLE); TIM_Cmd(TIM2, ENABLE); } // 调整舵机角度 void Servo_SetAngle(uint8_t angle) { uint16_t pulse 500 angle * (2000 / 180); TIM_SetCompare1(TIM2, pulse); }在无人机项目中我发现PWM输出有时会出现抖动。解决方法是在修改占空比时先关闭预装载修改后再启用TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Disable); TIM_SetCompare1(TIM2, new_pulse); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);4. UART通信从基础收发到协议解析4.1 串口初始化与中断接收UART是嵌入式系统最常用的通信接口。CH32的串口配置需要注意波特率计算和中断优先级设置void USART2_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; USART_InitTypeDef USART_InitStruct {0}; NVIC_InitTypeDef NVIC_InitStruct {0}; RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // TX配置为复用推挽输出 GPIO_InitStruct.GPIO_Pin GPIO_Pin_2; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStruct); // RX配置为浮空输入 GPIO_InitStruct.GPIO_Pin GPIO_Pin_3; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStruct); USART_InitStruct.USART_BaudRate 115200; USART_InitStruct.USART_WordLength USART_WordLength_8b; USART_InitStruct.USART_StopBits USART_StopBits_1; USART_InitStruct.USART_Parity USART_Parity_No; USART_InitStruct.USART_Mode USART_Mode_Tx | USART_Mode_Rx; USART_Init(USART2, USART_InitStruct); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); NVIC_InitStruct.NVIC_IRQChannel USART2_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority 2; NVIC_InitStruct.NVIC_IRQChannelSubPriority 0; NVIC_InitStruct.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStruct); USART_Cmd(USART2, ENABLE); }4.2 高效数据收发方案在实际项目中我总结了几种数据收发方案中断环形缓冲区适合不定长数据DMA传输适合大数据量空闲中断配合DMA实现帧解析这里分享一个环形缓冲区实现#define BUF_SIZE 256 typedef struct { uint8_t buffer[BUF_SIZE]; uint16_t head; uint16_t tail; } RingBuffer; RingBuffer rx_buf {0}; void USART2_IRQHandler(void) { if(USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART2); rx_buf.buffer[rx_buf.head] data; if(rx_buf.head BUF_SIZE) rx_buf.head 0; } } uint8_t USART_ReadByte(void) { if(rx_buf.head rx_buf.tail) return 0; uint8_t data rx_buf.buffer[rx_buf.tail]; if(rx_buf.tail BUF_SIZE) rx_buf.tail 0; return data; }在物联网网关项目中我发现串口通信偶尔会丢数据。解决方法是在硬件上增加120Ω终端电阻并在软件上添加奇偶校验和超时重传机制。

更多文章