CH32V30x 学习指南

从8051到RISC-V的平滑过渡 · 基于你的工程包定制

第一章 概述与思维转变

🎯 本指南特点

核心思维转变

⚠️ 最大的区别

8051:直接操作寄存器,上电就能用

CH32:库函数封装,必须先开时钟!

开发思维对比
flowchart LR subgraph A["8051 开发流程"] direction TB A1["💡 想法"] --> A2["📝 写寄存器"] A2 --> A3["✅ 完成"] end subgraph B["CH32 开发流程"] direction TB B1["💡 想法"] --> B2["🔓 开时钟 RCC"] B2 --> B3["📋 配置结构体"] B3 --> B4["⚙️ 调用Init函数"] B4 --> B5["✅ 完成"] end A --> |"思维升级"| B style A1 fill:#fef3c7 style A2 fill:#fef3c7 style A3 fill:#d1fae5 style B1 fill:#dbeafe style B2 fill:#fee2e2 style B3 fill:#dbeafe style B4 fill:#dbeafe style B5 fill:#d1fae5
8051 思维
// 点亮LED,一行搞定
P1 = 0xFE;
CH32 思维
// 点亮LED,四步流程
1. 开时钟 (RCC)
2. 配置结构体
3. 调用Init函数
4. 操作GPIO

概念映射表

功能 8051 (STC15) CH32V30x
GPIO输出 P0 = 0xFF GPIO_Write(GPIOA, 0xFF)
单引脚置高 P1_0 = 1 GPIO_SetBits(GPIOA, GPIO_Pin_0)
单引脚置低 P1_0 = 0 GPIO_ResetBits(GPIOA, GPIO_Pin_0)
读取引脚 val = P1_0 GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)
中断函数 void T0_ISR() interrupt 1 void TIM2_IRQHandler(void)
定时器配置 TMOD = 0x01; TH0=...; TL0=... TIM_TimeBaseInit(TIM2, &InitStruct)
启动定时器 TR0 = 1 TIM_Cmd(TIM2, ENABLE)
中断使能 ET0 = 1; EA = 1 NVIC_Init(&NVIC_InitStruct)
时钟控制 无需操作 RCC_APBxPeriphClockCmd(...)

架构对比

8051 架构(简单直接)
8051 CPU 统一总线 - 所有外设共享 P0 P1 P2 P3 定时器

✅ 简单直接:上电即用,无需时钟配置

CH32V30x 架构(模块化设计)
RISC-V CPU 96MHz 高性能 🔑 RCC 时钟控制器 (必须先开!) NVIC 中断控制器 SYSCLK (96MHz) APB2 总线 (96MHz) - 高速外设 GPIOA~E USART1 SPI1 ADC1,2 TIM1,8 TIM9,10 🔌 每个外设都有独立时钟开关 APB1 总线 (48MHz) - 低速外设 TIM2~7 USART2~5 SPI2,3 I2C1,2 CAN1,2 DAC 🔌 每个外设都有独立时钟开关

⚠️ 重要:使用任何外设前,必须先通过RCC开启对应的时钟!

第二章 时钟系统 (RCC)

🚨 这是8051开发者最容易忽略的!

在CH32中,每个外设在使用前必须先开启时钟,否则外设不会工作。这是为了降低功耗设计的。

RCC 基础概念

RCC (Reset and Clock Control) 是时钟控制器,负责:

时钟树详解

CH32V30x 时钟树
flowchart TB subgraph Sources["时钟源"] HSI["🔷 HSI
内部8MHz"] HSE["🔶 HSE
外部晶振8MHz"] end subgraph PLL["锁相环倍频"] PLLMUL["PLL倍频器
×12"] end subgraph System["系统时钟"] SYSCLK["⚡ SYSCLK
96MHz"] end subgraph Buses["总线时钟"] AHB["AHB
96MHz"] APB2["APB2 高速
96MHz"] APB1["APB1 低速
48MHz"] end subgraph APB2_Periph["APB2 外设"] GPIO["GPIOA~E"] USART1["USART1"] SPI1["SPI1"] ADC["ADC1,2"] TIM_H["TIM1,8,9,10"] end subgraph APB1_Periph["APB1 外设"] TIM_L["TIM2~7"] USART_L["USART2~5"] SPI_L["SPI2,3"] I2C["I2C1,2"] CAN["CAN1,2"] end HSI --> PLLMUL HSE --> PLLMUL PLLMUL --> SYSCLK SYSCLK --> AHB AHB --> APB2 AHB --> APB1 APB2 --> GPIO APB2 --> USART1 APB2 --> SPI1 APB2 --> ADC APB2 --> TIM_H APB1 --> TIM_L APB1 --> USART_L APB1 --> SPI_L APB1 --> I2C APB1 --> CAN style HSE fill:#fef3c7,stroke:#f59e0b style PLLMUL fill:#dbeafe,stroke:#3b82f6 style SYSCLK fill:#fee2e2,stroke:#ef4444 style APB2 fill:#d1fae5,stroke:#10b981 style APB1 fill:#fef9c3,stroke:#eab308

RCC 常用API

RCC_APB2PeriphClockCmd(uint32_t Periph, FunctionalState NewState)

开启/关闭 APB2 总线上的外设时钟

Periph 外设宏,如 RCC_APB2Periph_GPIOCRCC_APB2Periph_USART1
NewState ENABLEDISABLE
RCC_APB1PeriphClockCmd(uint32_t Periph, FunctionalState NewState)

开启/关闭 APB1 总线上的外设时钟

Periph 外设宏,如 RCC_APB1Periph_TIM3RCC_APB1Periph_USART2
💡 记忆技巧

APB2 = 高速 = GPIO全家 + USART1 + SPI1 + ADC + 高级定时器(TIM1,8)

APB1 = 低速 = TIM2~7 + USART2~5 + SPI2,3 + I2C

常用外设时钟宏

APB2 外设 (高速) APB1 外设 (低速)
RCC_APB2Periph_GPIOA RCC_APB1Periph_TIM2
RCC_APB2Periph_GPIOB RCC_APB1Periph_TIM3
RCC_APB2Periph_GPIOC RCC_APB1Periph_TIM4
RCC_APB2Periph_GPIOD RCC_APB1Periph_USART2
RCC_APB2Periph_USART1 RCC_APB1Periph_I2C1
RCC_APB2Periph_SPI1 RCC_APB1Periph_SPI2
RCC_APB2Periph_ADC1 RCC_APB1Periph_CAN1

第三章 GPIO 详解

GPIO 模式详解

模式 宏定义 用途 8051对应
推挽输出 GPIO_Mode_Out_PP LED、蜂鸣器等 P0口外接上拉
开漏输出 GPIO_Mode_Out_OD I2C、电平转换 P0口默认
浮空输入 GPIO_Mode_IN_FLOATING 外部有确定电平 P1~P3口
上拉输入 GPIO_Mode_IPU 按键(低电平有效) -
下拉输入 GPIO_Mode_IPD 按键(高电平有效) -
模拟输入 GPIO_Mode_AIN ADC采集 -
复用推挽 GPIO_Mode_AF_PP USART_TX、SPI等 -
复用开漏 GPIO_Mode_AF_OD I2C_SDA/SCL -
GPIO 输出模式对比
推挽输出 (Out_PP) VDD P-MOS 输出 N-MOS GND ✅ 可输出高/低电平 ✅ 驱动能力强 📍 用于LED、蜂鸣器 开漏输出 (Out_OD) 外部上拉 输出 N-MOS GND ⚠️ 只能拉低,需外部上拉 ✅ 支持线与逻辑 📍 用于I2C、电平转换

GPIO 初始化流程

GPIO 初始化四步流程
flowchart LR A["1️⃣ 开时钟
RCC_APB2PeriphClockCmd"] --> B["2️⃣ 定义结构体
GPIO_InitTypeDef"] B --> C["3️⃣ 填充成员
Pin/Mode/Speed"] C --> D["4️⃣ 调用Init
GPIO_Init()"] style A fill:#fee2e2,stroke:#ef4444,stroke-width:2px style B fill:#dbeafe,stroke:#3b82f6,stroke-width:2px style C fill:#d1fae5,stroke:#10b981,stroke-width:2px style D fill:#fef3c7,stroke:#f59e0b,stroke-width:2px
// GPIO初始化完整模板
void LED_Init(void)
{
    // 第1步:开启GPIOC时钟 【必须!8051无此步骤】
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

    // 第2步:定义GPIO初始化结构体
    GPIO_InitTypeDef GPIO_InitStructure = {0};

    // 第3步:填充结构体成员
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_2;          // PC2引脚
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_Out_PP;    // 推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;    // 翻转速度

    // 第4步:调用初始化函数
    GPIO_Init(GPIOC, &GPIO_InitStructure);
}

GPIO 常用API

GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

将指定引脚置高 (输出1)

// 等价于 8051 的 P1_0 = 1;
GPIO_SetBits(GPIOC, GPIO_Pin_2);
GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

将指定引脚置低 (输出0)

// 等价于 8051 的 P1_0 = 0;
GPIO_ResetBits(GPIOC, GPIO_Pin_2);
GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

读取输入引脚电平,返回 Bit_SET(1) 或 Bit_RESET(0)

// 等价于 8051 的 if(P1_0 == 0)
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == Bit_RESET) {
    // 按键按下
}

实战练习:LED闪烁

📝 练习1:让PC2引脚的LED每500ms闪烁一次

在你的工程 第一讲/v2.18/User/main.c 中实现

8051 实现
#include <STC15F2K60S2.H>

sbit LED = P2^0;

void main() {
    while(1) {
        LED = 0;
        Delay_ms(500);
        LED = 1;
        Delay_ms(500);
    }
}
CH32 实现
#include "debug.h"

void main(void) {
    // 系统初始化
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SystemCoreClockUpdate();
    Delay_Init();

    // GPIO初始化
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure = {0};
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    while(1) {
        GPIO_SetBits(GPIOC, GPIO_Pin_2);
        Delay_Ms(500);
        GPIO_ResetBits(GPIOC, GPIO_Pin_2);
        Delay_Ms(500);
    }
}

第四章 定时器与中断

CH32V30x 定时器分类

定时器类型与功能
🔥 高级定时器 TIM1, TIM8, TIM9, TIM10 PWM互补输出 死区控制、刹车 ⚡ 通用定时器 TIM2, TIM3, TIM4, TIM5 定时中断、PWM 输入捕获、编码器 ✅ 基本定时器 TIM6, TIM7 纯定时功能 DAC触发 APB2 (96MHz) APB1 (48MHz) 💡 入门推荐: TIM3

基本定时中断配置

🔢 定时时间计算公式

定时时间 = (ARR + 1) × (PSC + 1) / 时钟频率

例如:ARR=999, PSC=95, 时钟=96MHz

定时时间 = (999+1) × (95+1) / 96000000 = 1ms

定时器中断配置流程
flowchart TB A["1️⃣ 开启TIM时钟
RCC_APB1PeriphClockCmd"] --> B["2️⃣ 配置定时器
TIM_TimeBaseInit"] B --> C["3️⃣ 使能中断
TIM_ITConfig"] C --> D["4️⃣ 配置NVIC
NVIC_Init"] D --> E["5️⃣ 启动定时器
TIM_Cmd"] E --> F["6️⃣ 编写中断函数
TIMx_IRQHandler"] style A fill:#fee2e2,stroke:#ef4444 style B fill:#dbeafe,stroke:#3b82f6 style C fill:#fef3c7,stroke:#f59e0b style D fill:#d1fae5,stroke:#10b981 style E fill:#e0e7ff,stroke:#6366f1 style F fill:#fce7f3,stroke:#ec4899
8051 定时器配置
void Timer0_Init(void) {
    TMOD &= 0xF0;
    TMOD |= 0x01;  // 模式1
    TL0 = 0x18;
    TH0 = 0xFC;    // 1ms @12MHz
    TF0 = 0;
    TR0 = 1;       // 启动
    ET0 = 1;       // 中断使能
    EA = 1;        // 总中断
}

void T0_ISR() interrupt 1 {
    TH0 = 0xFC;
    TL0 = 0x18;
    // 处理代码
}
CH32 定时器配置
void TIM3_Init(void) {
    // 1. 开时钟
    RCC_APB1PeriphClockCmd(
        RCC_APB1Periph_TIM3, ENABLE);

    // 2. 配置定时器
    TIM_TimeBaseInitTypeDef TIM_InitStruct;
    TIM_InitStruct.TIM_Period = 1000-1;    // ARR
    TIM_InitStruct.TIM_Prescaler = 96-1;   // PSC
    TIM_InitStruct.TIM_ClockDivision = 0;
    TIM_InitStruct.TIM_CounterMode =
        TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM3, &TIM_InitStruct);

    // 3. 配置中断
    TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);

    // 4. 配置NVIC
    NVIC_InitTypeDef NVIC_InitStruct;
    NVIC_InitStruct.NVIC_IRQChannel = TIM3_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 3;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    // 5. 启动定时器
    TIM_Cmd(TIM3, ENABLE);
}

void TIM3_IRQHandler(void) {
    if(TIM_GetITStatus(TIM3, TIM_IT_Update)) {
        // 处理代码
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
    }
}

NVIC 中断控制器

NVIC 中断处理流程
flowchart LR subgraph Peripherals["外设中断源"] TIM["TIM2_IRQn
TIM3_IRQn
..."] USART["USART1_IRQn
USART2_IRQn
..."] EXTI["EXTI0_IRQn
EXTI1_IRQn
..."] end subgraph NVIC["NVIC 控制器"] direction TB PRIO["优先级判断
抢占 > 子优先级"] QUEUE["中断队列"] end subgraph CPU["CPU"] ISR["执行中断服务函数
TIMx_IRQHandler()"] end TIM --> NVIC USART --> NVIC EXTI --> NVIC NVIC --> CPU style PRIO fill:#fef3c7,stroke:#f59e0b style ISR fill:#d1fae5,stroke:#10b981

NVIC 优先级分组

分组 抢占优先级位数 子优先级位数 说明
NVIC_PriorityGroup_0 0 4 无抢占
NVIC_PriorityGroup_1 1 3 2级抢占
NVIC_PriorityGroup_2 2 2 ✅ 推荐使用
NVIC_PriorityGroup_3 3 1 8级抢占
NVIC_PriorityGroup_4 4 0 16级抢占

实战练习:定时器LED

📝 练习2:用TIM3中断实现1秒LED翻转

改造你的main.c,将Delay_Ms改为定时器中断方式

#include "debug.h"

// 中断函数声明(WCH特有属性)
void TIM3_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));

// 全局变量
volatile uint32_t uwTick = 0;
volatile uint8_t Led_Flag = 0;

// TIM3初始化:1ms中断
void TIM3_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct = {0};
    TIM_TimeBaseInitStruct.TIM_Period = 1000 - 1;
    TIM_TimeBaseInitStruct.TIM_Prescaler = 96 - 1;
    TIM_TimeBaseInitStruct.TIM_ClockDivision = 0;
    TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);

    TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);

    NVIC_InitTypeDef NVIC_InitStruct = {0};
    NVIC_InitStruct.NVIC_IRQChannel = TIM3_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 3;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    TIM_Cmd(TIM3, ENABLE);
}

// TIM3中断服务函数
void TIM3_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
    {
        uwTick++;
        if(uwTick >= 1000) {
            uwTick = 0;
            Led_Flag ^= 1;
            if(Led_Flag) GPIO_SetBits(GPIOC, GPIO_Pin_2);
            else GPIO_ResetBits(GPIOC, GPIO_Pin_2);
        }
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
    }
}

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SystemCoreClockUpdate();
    Delay_Init();
    USART_Printf_Init(115200);

    // LED初始化
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_2;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStruct);

    TIM3_Init();

    while(1) { }
}

第五章 串口通信

USART 初始化配置

USART 通信原理
CH32V30x USART3 TX: PB10 | RX: PB11 电脑 串口调试助手 115200-8-N-1 TX → RX RX ← TX USB转TTL

串口初始化需要配置三大块:GPIO(引脚复用)、USART(通信参数)、NVIC(中断优先级)。

USART 初始化四步走
flowchart LR A["1. 开时钟
RCC"] --> B["2. 配GPIO
复用推挽"] B --> C["3. 设USART参数
波特率/数据位等"] C --> D["4. 配NVIC
接收中断"] D --> E["5. 使能USART"] style A fill:#fee2e2,stroke:#ef4444 style B fill:#fef3c7,stroke:#f59e0b style C fill:#dbeafe,stroke:#3b82f6 style D fill:#e0e7ff,stroke:#6366f1 style E fill:#dcfce7,stroke:#22c55e
💡 关键知识点

USART1 挂在 APB2 上(高速总线),USART2~5 挂在 APB1 上(低速总线),开时钟时要注意区分!

串口TX引脚RX引脚时钟总线时钟宏
USART1PA9PA10APB2RCC_APB2Periph_USART1
USART2PA2PA3APB1RCC_APB1Periph_USART2
USART3PB10PB11APB1RCC_APB1Periph_USART3

下面以 USART3(PB10/PB11)为例,演示完整的串口初始化,包含接收中断:

#include "debug.h"

// 声明中断处理函数(WCH专用快速中断属性)
void USART3_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));

void Usart3_Init(void)
{
    GPIO_InitTypeDef  GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef  NVIC_InitStructure;

    /*---------- 第1步:开时钟 ----------*/
    // USART3 在 APB1 上,GPIO 在 APB2 上
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    /*---------- 第2步:配置GPIO ----------*/
    // PB10(TX) + PB11(RX) 都配为复用推挽
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10 | GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    /*---------- 第3步:配置USART参数 ----------*/
    USART_InitStructure.USART_BaudRate            = 115200;              // 波特率
    USART_InitStructure.USART_WordLength           = USART_WordLength_8b;   // 8位数据
    USART_InitStructure.USART_StopBits             = USART_StopBits_1;      // 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(USART3, &USART_InitStructure);

    // 使能接收中断
    USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);

    /*---------- 第4步:配置NVIC ----------*/
    NVIC_InitStructure.NVIC_IRQChannel                   = USART3_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    /*---------- 第5步:使能串口 ----------*/
    USART_Cmd(USART3, ENABLE);
}
与8051对比
配置项8051CH32V30x
波特率手动算 TH1/TL1 重装值直接填数字 115200
引脚固定 P3.0/P3.1每个USART有独立引脚,还可重映射
中断ES=1; EA=1;NVIC 分组 + 优先级配置
数量通常只有1个多达5个USART(USART1~5)

printf 重定向

💡 好消息

你的工程包中 Debug/debug.c 已经实现了 printf 重定向到 USART1,直接调用 USART_Printf_Init(115200) 即可使用 printf()

这是最快捷的调试串口方案,一行代码搞定:

#include "debug.h"

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SystemCoreClockUpdate();
    Delay_Init();

    // 一行搞定串口printf(USART1, PA9输出)
    USART_Printf_Init(115200);

    printf("Hello CH32V30x!\n");
    printf("System Clock: %d Hz\n", SystemCoreClock);

    int count = 0;
    while(1) {
        printf("Count: %d\n", count++);
        Delay_Ms(1000);
    }
}
两种串口方案对比
方案适用场景优点缺点
USART_Printf_Init调试输出一行搞定,支持printf占用USART1,仅发送
手动初始化USART与外设通信灵活配置,可收可发代码量多

接收处理(中断方式)

串口接收推荐使用中断方式,每收到一个字节自动触发 USART_IT_RXNE 中断。

串口接收中断流程
flowchart TB A["外部设备发送数据"] --> B["USART硬件接收"] B --> C{"RXNE标志置位?"} C -->|"是"| D["触发中断
USART3_IRQHandler"] D --> E["USART_ReceiveData
读取数据"] E --> F["处理数据
(存缓冲/执行命令)"] F --> G["清除中断标志"] style A fill:#fef3c7,stroke:#f59e0b style D fill:#fee2e2,stroke:#ef4444 style E fill:#dbeafe,stroke:#3b82f6 style F fill:#dcfce7,stroke:#22c55e
/*==================== 中断处理函数 ====================*/
void USART3_IRQHandler(void)
{
    u8 temp;
    if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)
    {
        // 读取接收到的数据(同时自动清除RXNE标志)
        temp = USART_ReceiveData(USART3);

        // 在这里处理接收到的数据
        // 例如:回显(收到什么就发回什么)
        USART_SendData(USART3, temp);
    }
    // 手动清除中断挂起位,确保不会重复触发
    USART_ClearITPendingBit(USART3, USART_IT_RXNE);
}
💡 注意事项

1. 中断函数名必须和启动文件中的向量表一致(如 USART3_IRQHandler),写错就进不了中断。

2. 必须加 __attribute__((interrupt("WCH-Interrupt-fast"))) 声明,这是 WCH RISC-V 的快速中断要求。

3. USART_ReceiveData() 读数据时会自动清除 RXNE 标志,但建议仍调用 USART_ClearITPendingBit() 确保清除。

下面是完整的发送+接收示例,每秒发送字符 'a',同时通过中断接收数据:

#include "debug.h"

// 中断函数声明
void USART3_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));

void Usart3_Init(void)
{
    GPIO_InitTypeDef  GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef  NVIC_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10 | GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOB, &GPIO_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(USART3, &USART_InitStructure);
    USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);

    NVIC_InitStructure.NVIC_IRQChannel                   = USART3_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    USART_Cmd(USART3, ENABLE);
}

/*---------- 接收中断 ----------*/
void USART3_IRQHandler(void)
{
    u8 temp;
    if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)
    {
        temp = USART_ReceiveData(USART3);
        // 回显:收到什么发回什么
        USART_SendData(USART3, temp);
    }
    USART_ClearITPendingBit(USART3, USART_IT_RXNE);
}

/*---------- 主函数 ----------*/
int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SystemCoreClockUpdate();
    Delay_Init();
    Usart3_Init();

    while(1)
    {
        // 每秒发送一个字符 'a'
        USART_SendData(USART3, 'a');
        Delay_Ms(1000);
    }
}
USART 常用 API 速查
功能函数
初始化USART_Init(USARTx, &InitStruct)
使能串口USART_Cmd(USARTx, ENABLE)
发送一字节USART_SendData(USARTx, data)
接收一字节USART_ReceiveData(USARTx)
使能中断USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE)
查中断状态USART_GetITStatus(USARTx, USART_IT_RXNE)
清中断标志USART_ClearITPendingBit(USARTx, USART_IT_RXNE)

第六章 综合项目

移植你的任务调度器

任务调度器架构
flowchart TB subgraph HW["硬件层"] TIM["TIM3
1ms中断"] end subgraph TICK["时基层"] UWTICK["uwTick++
全局时间戳"] end subgraph SCHED["调度器"] direction LR CHECK["检查各任务
是否到期"] EXEC["执行到期任务"] end subgraph TASKS["任务列表"] T1["Led_Task
500ms"] T2["Key_Task
10ms"] T3["Display_Task
100ms"] T4["Sensor_Task
200ms"] end TIM --> |"中断"| UWTICK UWTICK --> SCHED SCHED --> TASKS style TIM fill:#fee2e2,stroke:#ef4444 style UWTICK fill:#fef3c7,stroke:#f59e0b style CHECK fill:#dbeafe,stroke:#3b82f6 style EXEC fill:#d1fae5,stroke:#10b981
#include "debug.h"

/*==================== 任务调度器 ====================*/

volatile uint32_t uwTick = 0;

typedef struct {
    void (*task_func)(void);
    uint32_t rate_ms;
    uint32_t last_ms;
} task_t;

void Led_Task(void);
void Key_Task(void);
void Display_Task(void);

task_t Scheduler_Task[] = {
    {Led_Task,      500,  0},
    {Key_Task,      10,   0},
    {Display_Task,  100,  0},
};

const uint8_t task_num = sizeof(Scheduler_Task) / sizeof(task_t);

void Scheduler_Run(void)
{
    for(uint8_t i = 0; i < task_num; i++) {
        uint32_t now = uwTick;
        if(now - Scheduler_Task[i].last_ms >= Scheduler_Task[i].rate_ms) {
            Scheduler_Task[i].last_ms = now;
            Scheduler_Task[i].task_func();
        }
    }
}

/*==================== 任务实现 ====================*/

uint8_t led_state = 0;

void Led_Task(void) {
    led_state ^= 1;
    if(led_state) GPIO_SetBits(GPIOC, GPIO_Pin_2);
    else GPIO_ResetBits(GPIOC, GPIO_Pin_2);
}

void Key_Task(void) {
    // 按键扫描
}

void Display_Task(void) {
    static uint32_t count = 0;
    printf("Tick: %lu\n", count++);
}

/*==================== 主函数 ====================*/

void TIM3_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));

void TIM3_IRQHandler(void) {
    if(TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
        uwTick++;
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
    }
}

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SystemCoreClockUpdate();
    Delay_Init();
    USART_Printf_Init(115200);

    // GPIO/TIM初始化...

    while(1) {
        Scheduler_Run();
    }
}

工程模板结构建议

推荐的工程目录结构
flowchart TB ROOT["📁 你的CH32工程"] --> CORE["📁 Core
RISC-V内核"] ROOT --> DEBUG["📁 Debug
调试工具"] ROOT --> PERIPH["📁 Peripheral
官方外设库"] ROOT --> STARTUP["📁 Startup
启动文件"] ROOT --> DRIVER["📁 Driver 🆕
你的驱动层"] ROOT --> APP["📁 App 🆕
应用层"] ROOT --> USER["📁 User
主程序"] DRIVER --> D1["bsp_led.c/h"] DRIVER --> D2["bsp_key.c/h"] DRIVER --> D3["bsp_timer.c/h"] APP --> A1["scheduler.c/h"] APP --> A2["task_led.c/h"] APP --> A3["task_key.c/h"] USER --> U1["main.c"] USER --> U2["ch32v30x_it.c"] style ROOT fill:#eff6ff,stroke:#3b82f6 style DRIVER fill:#dcfce7,stroke:#22c55e style APP fill:#fef3c7,stroke:#f59e0b

附录

API 速查表

GPIO

功能函数
初始化GPIO_Init(GPIOx, &InitStruct)
置高GPIO_SetBits(GPIOx, GPIO_Pin_x)
置低GPIO_ResetBits(GPIOx, GPIO_Pin_x)
写位GPIO_WriteBit(GPIOx, GPIO_Pin_x, BitVal)
写端口GPIO_Write(GPIOx, PortVal)
读输入GPIO_ReadInputDataBit(GPIOx, GPIO_Pin_x)

定时器

功能函数
基本初始化TIM_TimeBaseInit(TIMx, &InitStruct)
使能定时器TIM_Cmd(TIMx, ENABLE)
中断配置TIM_ITConfig(TIMx, TIM_IT_Update, ENABLE)
获取中断状态TIM_GetITStatus(TIMx, TIM_IT_Update)
清除中断标志TIM_ClearITPendingBit(TIMx, TIM_IT_Update)

USART

功能函数
初始化USART_Init(USARTx, &InitStruct)
使能串口USART_Cmd(USARTx, ENABLE)
发送一字节USART_SendData(USARTx, data)
接收一字节USART_ReceiveData(USARTx)
使能中断USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE)
查中断状态USART_GetITStatus(USARTx, USART_IT_RXNE)
清中断标志USART_ClearITPendingBit(USARTx, USART_IT_RXNE)
快捷printf初始化USART_Printf_Init(baud) (仅USART1)

常见问题排查

GPIO不工作排查流程
flowchart TB START["GPIO不工作"] --> Q1{"开了RCC时钟?"} Q1 -->|"否"| A1["添加 RCC_APB2PeriphClockCmd"] Q1 -->|"是"| Q2{"模式正确?"} Q2 -->|"否"| A2["检查 GPIO_Mode"] Q2 -->|"是"| Q3{"引脚正确?"} Q3 -->|"否"| A3["检查 GPIO_Pin"] Q3 -->|"是"| Q4{"硬件连接?"} Q4 -->|"否"| A4["检查接线"] Q4 -->|"是"| A5["检查其他代码逻辑"] style START fill:#fee2e2,stroke:#ef4444 style A1 fill:#dcfce7,stroke:#22c55e style A2 fill:#dcfce7,stroke:#22c55e style A3 fill:#dcfce7,stroke:#22c55e style A4 fill:#dcfce7,stroke:#22c55e
❌ 问题1:GPIO不工作

症状:设置了GPIO但引脚无输出

原因:99%是忘记开时钟

解决:检查 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOx, ENABLE)

❌ 问题2:中断不触发

检查清单

  1. 外设时钟是否开启?
  2. 外设中断是否使能?(TIM_ITConfig)
  3. NVIC是否配置?(NVIC_Init)
  4. 中断函数名是否正确?
  5. 是否添加了 __attribute__((interrupt("WCH-Interrupt-fast")))
❌ 问题3:printf无输出

检查清单

  1. 是否调用了 USART_Printf_Init(115200)
  2. 串口波特率是否匹配?
  3. TX引脚接线是否正确?(默认PA9)
❌ 问题4:串口收不到数据

检查清单

  1. USART 和 GPIO 时钟都开了吗?(注意 USART1 在 APB2,USART2~5 在 APB1)
  2. GPIO 模式是否设为 GPIO_Mode_AF_PP(复用推挽)?
  3. 是否使能了接收中断 USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE)
  4. NVIC 是否配置?中断通道号是否正确(如 USART3_IRQn)?
  5. 中断函数名是否与向量表一致?是否添加了 __attribute__((interrupt("WCH-Interrupt-fast")))
  6. 是否调用了 USART_Cmd(USARTx, ENABLE) 使能串口?
  7. TX/RX 接线是否交叉连接?(MCU的TX接对方RX,反之亦然)

📚 CH32V30x 学习指南 · 基于 v2.18 工程包

祝你学习顺利!🚀