CH32V30x 学习指南
从8051到RISC-V的平滑过渡 · 基于你的工程包定制
第一章 概述与思维转变
- 基于你已有的 STC15F2K60S2 (8051) 经验
- 针对你的 CH32V30x v2.18 工程包定制
- 代码可直接在你的工程中运行
- 循序渐进,每章都有实战练习
核心思维转变
8051:直接操作寄存器,上电就能用
CH32:库函数封装,必须先开时钟!
// 点亮LED,一行搞定 P1 = 0xFE;
// 点亮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(...) |
架构对比
✅ 简单直接:上电即用,无需时钟配置
⚠️ 重要:使用任何外设前,必须先通过RCC开启对应的时钟!
第二章 时钟系统 (RCC)
在CH32中,每个外设在使用前必须先开启时钟,否则外设不会工作。这是为了降低功耗设计的。
RCC 基础概念
RCC (Reset and Clock Control) 是时钟控制器,负责:
- 系统时钟配置(HSI/HSE/PLL)
- 总线时钟分频(AHB/APB1/APB2)
- 外设时钟门控(开启/关闭各外设时钟)
时钟树详解
内部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
开启/关闭 APB2 总线上的外设时钟
RCC_APB2Periph_GPIOC、RCC_APB2Periph_USART1
ENABLE 或 DISABLE
开启/关闭 APB1 总线上的外设时钟
RCC_APB1Periph_TIM3、RCC_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 初始化流程
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
将指定引脚置高 (输出1)
// 等价于 8051 的 P1_0 = 1; GPIO_SetBits(GPIOC, GPIO_Pin_2);
将指定引脚置低 (输出0)
// 等价于 8051 的 P1_0 = 0; GPIO_ResetBits(GPIOC, GPIO_Pin_2);
读取输入引脚电平,返回 Bit_SET(1) 或 Bit_RESET(0)
// 等价于 8051 的 if(P1_0 == 0) if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == Bit_RESET) { // 按键按下 }
实战练习:LED闪烁
在你的工程 第一讲/v2.18/User/main.c 中实现
#include <STC15F2K60S2.H> sbit LED = P2^0; void main() { while(1) { LED = 0; Delay_ms(500); LED = 1; Delay_ms(500); } }
#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 定时器分类
基本定时中断配置
定时时间 = (ARR + 1) × (PSC + 1) / 时钟频率
例如:ARR=999, PSC=95, 时钟=96MHz
定时时间 = (999+1) × (95+1) / 96000000 = 1ms
RCC_APB1PeriphClockCmd"] --> B["2️⃣ 配置定时器TIM_TimeBaseInit"]
B --> C["3️⃣ 使能中断TIM_ITConfig"]
C --> D["4️⃣ 配置NVICNVIC_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
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; // 处理代码 }
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 中断控制器
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
改造你的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 初始化配置
串口初始化需要配置三大块:GPIO(引脚复用)、USART(通信参数)、NVIC(中断优先级)。
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引脚 | 时钟总线 | 时钟宏 |
|---|---|---|---|---|
| USART1 | PA9 | PA10 | APB2 | RCC_APB2Periph_USART1 |
| USART2 | PA2 | PA3 | APB1 | RCC_APB1Periph_USART2 |
| USART3 | PB10 | PB11 | APB1 | RCC_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 | CH32V30x |
|---|---|---|
| 波特率 | 手动算 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 中断。
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_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) |
第六章 综合项目
移植你的任务调度器
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(); } }
工程模板结构建议
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但引脚无输出
原因:99%是忘记开时钟
解决:检查 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOx, ENABLE)
检查清单:
- 外设时钟是否开启?
- 外设中断是否使能?(
TIM_ITConfig) - NVIC是否配置?(
NVIC_Init) - 中断函数名是否正确?
- 是否添加了
__attribute__((interrupt("WCH-Interrupt-fast")))?
检查清单:
- 是否调用了
USART_Printf_Init(115200)? - 串口波特率是否匹配?
- TX引脚接线是否正确?(默认PA9)
检查清单:
- USART 和 GPIO 时钟都开了吗?(注意 USART1 在 APB2,USART2~5 在 APB1)
- GPIO 模式是否设为
GPIO_Mode_AF_PP(复用推挽)? - 是否使能了接收中断
USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE)? - NVIC 是否配置?中断通道号是否正确(如
USART3_IRQn)? - 中断函数名是否与向量表一致?是否添加了
__attribute__((interrupt("WCH-Interrupt-fast")))? - 是否调用了
USART_Cmd(USARTx, ENABLE)使能串口? - TX/RX 接线是否交叉连接?(MCU的TX接对方RX,反之亦然)
📚 CH32V30x 学习指南 · 基于 v2.18 工程包
祝你学习顺利!🚀