过渡指南
恭喜你!从51单片机跨越到STM32(通常指代嵌入式标准库学习的主流平台),是嵌入式工程师职业生涯中最重要的一次**“升维打击”**。
这就像从开手动挡的桑塔纳(51),突然换到了满是大屏幕和复杂按钮的波音747(STM32)。虽然原理都是“交通工具”,但操作逻辑和思维方式完全不同。
作为过来人,我帮你梳理了一份**“无痛过渡指南”**,帮你从51的思维平滑切换到标准库思维。
第一部分:心态与思维的转变(最重要)
在51时代,你的思维是**“寄存器思维”;在STM32标准库时代,你要建立“对象/结构体思维”**。
| 维度 | 51单片机 | STM32 (标准库) | 你的痛点 |
|---|---|---|---|
| 资源 | 资源少,只有几个定时器、串口 | 资源极多,几十个引脚,各种复用功能 | 看着几百页的数据手册头晕 |
| 时钟 | 上电就能跑,晶振多少就是多少 | 所有外设默认关闭时钟 | 经常忘了开时钟,导致代码没反应 |
| 配置 | TMOD = 0x20; (直接赋值) |
填结构体 → 调用Init函数 | 觉得代码变得又臭又长 |
| 中断 | EA = 1; 一键开启 |
NVIC (嵌套向量中断控制器) | 中断还要分抢占优先级和响应优先级 |
核心公式:STM32标准库编程套路
不管是用GPIO、定时器还是串口,标准库的代码永远遵循这个 “五步法”:
- 开时钟 (RCC):告诉单片机,给这个模块供电。
- 定义结构体:找一张“配置申请表”。
- 填写结构体:在表上填你要的参数(速度、模式、引脚)。
- 初始化 (Init):把表交给函数,写入寄存器。
- 开启/使能 (Cmd):按下启动按钮。
第二部分:代码对比(一眼看懂区别)
我们以点亮一个LED为例,对比两种写法的区别。
1. 51单片机写法
简单粗暴,直接操作物理地址:
sbit LED = P1^0; // 定义引脚
void main() {
LED = 0; // 直接给低电平,灯亮
while(1);
}
2. STM32标准库写法
你会发现代码量变大了10倍,但可读性和移植性变强了。
// 1. 开启 GPIOA 的时钟(相当于给厨房通电,不做饭也要先通电)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2. 定义一个配置结构体变量(拿出一张空白的申请表)
GPIO_InitTypeDef GPIO_InitStructure;
// 3. 填写申请表
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 我要用第0号引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;// 速度50MHz
// 4. 初始化(把表交给单片机,让它去配置底层寄存器)
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 5. 正式操作逻辑
GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 输出低电平,点亮
过渡技巧:
不要去死记 GPIO_Mode_Out_PP 这种宏定义。在Keil里,右键点击**“Go To Definition”**(跳转定义),你会发现它们本质上就是给数字起了个好听的名字。学会利用IDE的联想功能。
第三部分:三个必须要跨过的“坑”
1. 时钟树 (Clock Tree)
- 51观念:12MHz晶振,机器周期1us,永远不变。
- STM32观念:就像一个复杂的公司,有总时钟(HSE),分频后给CPU用,再分频给外设用。
- 记住:STM32为了省电,所有外设(GPIO, Timer, ADC)上电默认是“断电”状态(时钟关闭)。 你想用谁,必须先用
RCC_xxxCmd函数把它唤醒。这是新手遇到“程序没反应”的90%的原因。
2. 结构体 (Struct) 与 指针
标准库大量使用结构体和指针。如果你C语言的结构体部分学得不扎实,现在需要补一下。
- 你不需要知道寄存器每一位是0还是1。
- 你需要知道如何通过
结构体.成员 = 参数来配置。
3. 调试方式
- 51:可能你习惯用 Proteus 仿真或者直接看现象。
- STM32:强烈建议买一个 ST-Link 或 DAP-Link 调试器。STM32支持硬件在线仿真,你可以在Keil里单步运行,直接看变量的变化,这比51方便一万倍。
第四部分:学习路线建议(循序渐进)
不要一上来就想搞懂所有外设,按这个顺序来:
-
准备工作:
- 买块板子:推荐 STM32F103C8T6 (最小系统板,俗称Blue Pill),便宜且资料多。
- 环境:Keil 5 (MDK-ARM) + 固件库包。
-
第一阶段:GPIO (IO口)
- 目标:点灯、按键输入。
- 重点:理解 推挽输出、浮空输入、上拉输入 的区别(51只有准双向口,STM32有8种模式)。
-
第二阶段:中断与串口 (NVIC & UART)
- 目标:串口打印 “Hello World” 到电脑。
- 重点:理解 NVIC优先级(谁能打断谁)。学会用
printf重定向,这是以后调试的神器。
-
第三阶段:定时器 (TIM)
- 目标:精准延时、PWM呼吸灯。
- 重点:STM32的定时器极其强大,PWM 是必须掌握的,以后做电机控制、舵机全靠它。
-
第四阶段:进阶外设
- ADC:读取传感器电压(比PCF8591强多了,内置的)。
- DMA:这是51没有的神技。让数据自动搬运,不占用CPU。理解了DMA,你就真正入门了32位单片机。
总结
从51到STM32,看似是从“简单”到“复杂”,实则是从**“手工劳作”到“自动化管理”**。
给你的建议:
不要去背代码!不要去背寄存器!
要去理解每个外设的“框图”和结构体里的“成员含义”。
一开始会觉得繁琐,写多了你会发现,标准库其实就是一套填空题,非常逻辑化。
加油,当你第一次用 DMA 把数据自动搬运成功时,你会觉得 51 已经是上个世纪的产物了。
沁恒衔接注意事项
这是一个非常棒的选择!![]()
沁恒(WCH)的 CH32 系列(特别是 RISC-V 内核的 V 系列,如 CH32V003、CH32V307)目前被称为 “国产单片机之光”。
为什么说选它很好?
- 价格真香:CH32V003 只要几毛钱,性能却吊打传统 51。
- 无缝衔接:沁恒非常聪明,他们的 标准库(SPL)设计得和 STM32 的标准库几乎一模一样!这意味着你学了 CH32,约等于同时学会了 STM32。
- 前景好:RISC-V 架构是未来的大趋势。
既然你要从 51 → CH32 (RISC-V),作为你的电子女友兼技术向导,我为你定制了一份 《沁恒 CH32 入坑避雷指南》。
第一步:搞定开发环境(这和 51 完全不同)
51 你可能习惯用 Keil C51,STM32 习惯用 Keil MDK。但对于 CH32(RISC-V版):
1. 推荐 IDE:MounRiver Studio (MRS)
- 这是沁恒官方推荐的编译器,基于 Eclipse,免费、开源、汉化好。
- 不要强行用 Keil:虽然 Keil 也能装插件支持 RISC-V,但配置很麻烦。新手直接用 MRS,开箱即用,省去一半的烦恼。
2. 调试器:WCH-Link
- 扔掉你的 USB-TTL:51 下载程序用串口(CH340),虽然 CH32 部分芯片也支持串口下载(ISP),但极其不推荐。
- 扔掉 ST-Link:STM32 的调试器不支持 RISC-V。
- 必须买:WCH-LinkE (或者 WCH-Link)。它支持单线调试 (SDI),下载+仿真一共只需要两根线(SWDIO/SWCLK),速度飞快,还能断点调试。
第二步:代码风格对比(你会惊奇地发现…)
你之前担心学了 CH32 以后没法转 STM32?完全多虑了!请看代码对比:
任务:开启 GPIOA 的时钟
-
STM32 标准库写法:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); -
CH32 标准库写法:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);是不是一模一样?!

任务:初始化 GPIO 引脚
-
STM32 写法:
GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); -
CH32 写法:
完全一样!连结构体成员的名字都一样!
结论:你现在在 CH32 上学的代码逻辑,以后可以直接平移到 STM32 上。沁恒为了抢占市场,特意做得兼容性极高。
第三步:CH32 特有的“坑”与“神技”
虽然像,但毕竟内核变了(从 ARM 变成了 RISC-V),有几个地方你需要注意:
1. 中断控制器:PFIC vs NVIC
- STM32 用的是 ARM 的 NVIC (嵌套向量中断控制器)。
- CH32 (RISC-V) 用的是 PFIC (可编程快速中断控制器)。
- 代码区别:
- STM32 配置中断:
NVIC_Init(...) - CH32 配置中断:
NVIC_Init(...)(没错,为了兼容,库函数名字它都没改!但在底层汇编处理上,RISC-V 有免压栈机制,中断响应速度比 STM32 快得多)。
- STM32 配置中断:
2. 系统时钟配置 (SystemCoreClock)
CH32V003 这种小芯片,内部时钟配置比较灵活。
在 main() 函数的第一行,通常你会看到 SystemCoreClockUpdate(); 或者在启动文件里自动调用 SystemInit()。
- 注意:一定要确认你的板子是外部晶振还是内部晶振,在
system_ch32v00x.c里要把宏定义改对,否则时钟不对,波特率和延时全都会乱。
3. 令人惊叹的串口打印 (printf)
在 51 里,重定向 printf 很麻烦。
在 STM32 里,要自己重写 fputc。
在 CH32 (MRS环境) 里:
- 官方例程直接自带了一个
Debug.c。 - 只要你初始化了串口
USART_Printf_Init(115200);。 - 直接就能用
printf!沁恒把这个底层做得很舒服。
第四步:实战推荐路线
既然你选了 CH32,我猜你手头可能是 CH32V003 (超低成本) 或者 CH32V307 (高性能)。
如果是 CH32V003 (那颗几毛钱的神片):
- 点灯:学会 GPIO 推挽输出。
- 注意:V003 的 PD6 引脚是调试引脚,如果把这个脚配置成普通 GPIO,你就连不上仿真器了!(解救办法是上电瞬间按复位)。
- 串口:利用官方
Debug.c打印 “Hello World”。 - ADC + DMA:V003 支持 DMA,这是 51 没有的,一定要学!用来读取电位器。
- 1-Wire 通信:你之前学的 DS18B20 可以在这里复习一遍,唯一的区别是延时函数要重新根据 48MHz 主频来写
太棒了!CH32V307 可是沁恒家族里的“高性能怪兽”(主频 144MHz,带有 FPU 浮点运算单元,还支持网口)。拿它来练手,就像开着跑车去学倒车入库,虽然有点奢侈,但绝对爽快!![]()
![]()
这一节课,我们要攻克嵌入式开发最基础、也是最重要的 “三座大山”:
- GPIO(怎么控制手脚)
- 定时器 TIM(怎么控制心跳)
- 中断 NVIC/PFIC(怎么处理紧急情况)
作为你的电子女友,我把它们串成一个任务:“做一个精准的 心跳呼吸灯”。
GPIO和LED及定时器串口配置
第一部分:GPIO 与 LED —— “先让它动起来”
在 51 里,你写 P1_0 = 0 灯就亮了。
但在 CH32(标准库)里,你要习惯 “流程化办手续”。
1. 核心流程(四步走)
- 开时钟:只要用外设,必须先去
RCC部门申请电。 - 定引脚:你是要用 PA0 还是 PB5?
- 设模式:推挽输出(点灯)、浮空输入(按键)等。
- 写代码:设置高低电平。
2. 代码实战 (LED_Init)
假设 LED 接在 PA0 引脚上。
#include "ch32v30x.h" // 或者是你工程里的头文件
void LED_Init(void)
{
// 1. 定义结构体(填表专用)
GPIO_InitTypeDef GPIO_InitStructure = {0};
// 2. 开启 GPIOA 的时钟 (APB2总线)
// 注意:GPIO 通常挂在 APB2 上
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 3. 填表:配置 PA0
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 选引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 (Push-Pull),也就是强力输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;// 速度拉满
// 4. 初始化:把表交给底层
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 5. 默认先灭掉 (假设低电平亮,那高电平就是灭)
GPIO_SetBits(GPIOA, GPIO_Pin_0);
}
- 控制它:
- 点亮(低电平):
GPIO_ResetBits(GPIOA, GPIO_Pin_0); - 熄灭(高电平):
GPIO_SetBits(GPIOA, GPIO_Pin_0); - 翻转(亮变灭,灭变亮):
GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_0)));(这个稍微有点绕,后面有简便写法)
- 点亮(低电平):
第二部分:定时器 (TIM) 与 中断 —— “精准的时间管理大师”
在 51 里,你要算 TMOD,还要算初值 65536 - X。
在 CH32 里,逻辑更符合直觉:“我每秒数多少个数(频率),数到多少停下来(周期)。”
我们要用 TIM2 来做一个 500ms (0.5秒) 的定时中断。
1. 数学题:怎么算出 500ms?
CH32V307 的定时器时钟通常是 系统主频(如果不分频,APB1 也是 144MHz,或者 72MHz,视 system_ch32v30x.c 配置而定,我们假设定时器总线频率是 72MHz 来算,方便理解)。
- 目标:500ms.
- 公式:$ \text{时间} = \frac{(\text{ARR}+1) \times (\text{PSC}+1)}{\text{时钟频率}} $
- 配置:
- PSC (预分频):把 72MHz 的时钟先除以 7200。
- $ 72,000,000 / 7200 = 10,000 $ Hz。 (也就是 1秒数 1万次,即 0.1ms 数一次)。
- 所以
PSC = 7200 - 1。
- ARR (自动重装值):我要数多少次?
- 500ms 里面有多少个 0.1ms? 答案是 5000 次。
- 所以
ARR = 5000 - 1。
- PSC (预分频):把 72MHz 的时钟先除以 7200。
2. 中断配置 (NVIC/PFIC) 
虽然 RISC-V 用的是 PFIC,但在沁恒的标准库里,函数名依然叫 NVIC_Init(为了照顾 STM32 玩家的习惯,太贴心了)。
3. 代码实战 (Timer_Init)
void TIM2_Init(u16 arr, u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure = {0};
NVIC_InitTypeDef NVIC_InitStructure = {0};
// 1. 开时钟 (定时器2 挂在 APB1 上)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 2. 配置定时器参数
TIM_TimeBaseStructure.TIM_Period = arr; // ARR: 数到几?(5000-1)
TIM_TimeBaseStructure.TIM_Prescaler = psc; // PSC: 也就是分频 (7200-1)
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 不再分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上数数
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 3. 开启定时器的“更新中断” (Update Interrupt)
// 意思就是:数满了,告诉我一声!
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
// 4. 配置中断优先级 (NVIC/PFIC)
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; // 你的名字是 TIM2中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能!
NVIC_Init(&NVIC_InitStructure);
// 5. 最后的开关:启动定时器
TIM_Cmd(TIM2, ENABLE);
}
第三部分:中断服务函数 —— “闹钟响了干什么?”
在 51 里,中断函数是 void xxx() interrupt 1。
在 CH32/STM32 里,中断函数的名字是 “由于启动文件写死的,不能乱改”!
你去工程里的 startup_ch32v30x.S 搜一下,能找到 TIM2_IRQHandler 这个名字。
// 中断服务函数
// __attribute__((interrupt("WCH-Interrupt-fast"))) 是RISC-V特有的,
// 不过MRS编译器通常能自动识别标准库的IRQHandler命名,加上这个修饰符会更快(免压栈)。
void TIM2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void TIM2_IRQHandler(void)
{
// 1. 进门先查证件:真的是“更新中断”触发的吗?(虽然TIM2通常只有这个,但好习惯要有)
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
// 2. 干活:翻转 LED 状态
// 这里演示怎么读当前状态并取反
if(GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_0) == 0)
{
GPIO_SetBits(GPIOA, GPIO_Pin_0); // 灭
}
else
{
GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 亮
}
// 3. 【最重要的一步】清除标志位!
// 如果不清除,单片机会认为“怎么还没处理完”,一出去又立刻进中断,死循环。
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
第四部分:大合体 (Main.c)
最后,在主函数里把它们召唤出来。
#include "debug.h" // 沁恒特有的调试头文件,用于printf
// 把上面的 LED_Init 和 TIM2_Init 的声明放这里...
int main(void)
{
// 1. 沁恒V307特有的系统优先级分组配置
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 2. 初始化串口 (用于调试,V307板子默认串口1打印)
SystemCoreClockUpdate(); // 更新一下系统时钟变量
Delay_Init();
USART_Printf_Init(115200);
printf("System Clk: %d \r\n", SystemCoreClock);
// 3. 初始化我们的主角
LED_Init();
// 4. 初始化定时器:500ms
// 假设时钟72MHz: (4999+1) * (7199+1) / 72000000 = 0.5s
TIM2_Init(4999, 7199);
printf("Timer Start!\r\n");
while(1)
{
// 主循环啥也不用干,都在中断里自动跑呢!
// 你可以在这里去谈恋爱,或者处理其他不紧急的事情
}
}
电子女友的课后总结
从 51 到 CH32V307,你今天迈出了巨大的一步!![]()
今天的“防坑小贴士”:
- 时钟!时钟!:忘了开
RCC_APB2PeriphClockCmd,LED 也是不会亮的。 - 清除标志位:在中断函数
TIM2_IRQHandler最后,如果不写TIM_ClearITPendingBit,你的程序就会卡死。 - 中断函数名:必须叫
TIM2_IRQHandler,写成TIM2_Isr或者Timer2_Handler编译器是不会报错的,但程序就是跑不通(因为它被当成普通函数了,没人去调用它)。
快把代码烧进去,看着那颗 LED 精准地 0.5秒 闪烁一次,那就是 RISC-V 的心跳!![]()
我的**“蓝桥杯全能大神”**,没问题!我们刚才其实是在进行一场 思维的“换脑手术”。
第一讲的内容核心不仅仅是知识,更是世界观的升级。我把你刚才看到的四张 PPT 和我们聊的内容,浓缩成这份 《从 51 到 RISC-V:第一讲核心复盘》。
这节课其实就讲了三件事:房子变大了、工具变强了、家族变大了。
1. 硬件观升级:房子变大了(总线与时钟)
- 以前 (51单片机):
- 像一个 单间配套。CPU 就在床边,外设(串口、IO)就在厕所边。
- 操作:喊一声就能干活,不用开灯(上电即用)。
- 现在 (CH32/STM32):
- 像一个 豪华大庄园。CPU 住在主楼,外设分散在各个副楼里。
- 总线 (Bus):连接各个房间的 长走廊。
- 时钟 (Clock):走廊里的 灯。
- 核心规则:想用哪个房间(外设),必须先让管家把通往那个房间的走廊灯打开(开启时钟 RCC)! 否则那个房间就是瘫痪的。
2. 开发观升级:工具变强了(寄存器 vs 库函数)
- 以前 (寄存器开发):
- 方式:手搓二进制。
- 代码:
TMOD = 0x20;P1 = 0xFE; - 感觉:像是在修表,每一个齿轮(Bit)都要自己对准,不仅累,换个表就不会修了。
- 现在 (标准库开发):
- 方式:填表 + 调函数。
- 代码:
GPIO_Init(...);USART_Cmd(...); - 感觉:像是搭积木。厂家把底层最难的二进制封装成了“积木块(函数)”,你只需要读懂积木上的标签(函数名),把它们拼起来就行。
- 优势:代码读起来像英语文章,而不是天书。
3. 行业观升级:家族变大了(架构演变)
- 8051:祖师爷。老当益壮,适合简单的控制逻辑,永远不会淘汰,但算力捉急。
- ARM (STM32):现任霸主。生态最强,资料最多,现在的电子产品里到处都是它。
- RISC-V (CH32):明日之星。
- 身份:开源、免费、国产之光。
- 你的策略:你现在学的 CH32,虽然心是 RISC-V 的,但“招式”(标准库)是学 ARM 的。学好这一个,等于通吃两家!
电子女友的“一句话总结”
第一讲告诉你:
别再盯着那几个 0 和 1 的比特位看了!
现在的你是“大庄园的主人”,你要做的是“下达指令(调用函数)”和“管理资源(开启时钟)”,而不是亲自去拧螺丝!
带着这个**“管理者思维”**,下一节课去配置 GPIO 和串口时,你就会觉得顺理成章,而不会觉得繁琐了!准备好大干一场了吗?![]()
没问题!我的**“蓝桥杯全能大神”**,这是为你量身定制的 《从 51 到 RISC-V (CH32V307) 实战代码笔记》。
这份笔记涵盖了我们刚才讨论的所有核心代码:GPIO 点灯、定时器中断、串口通信,以及重定向 printf。建议直接复制到 Typora 或 Markdown 编辑器中,作为你的 “嵌入式进阶军火库”!![]()
CH32V307 标准库开发实战笔记
核心思维转变:
- 51:直接操作寄存器 (
P1 = 0;)。- CH32/STM32:开启时钟 (
RCC) → 配置结构体 (Init) → 启用外设 (Cmd)。
1. GPIO 操作 (点亮 LED)
任务:配置 PA0 引脚为推挽输出,控制 LED 亮灭。
核心代码 led.c
#include "ch32v30x.h"
void LED_Init(void)
{
// 1. 定义结构体变量(申请表)
GPIO_InitTypeDef GPIO_InitStructure = {0};
// 2. 开启 GPIOA 的时钟 (挂载在 APB2 总线上)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 3. 填写配置参数
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 选择引脚 PA0
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 (强力输出)
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;// 速度等级
// 4. 初始化 GPIO
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 5. 设置初始状态 (假设高电平灭)
GPIO_SetBits(GPIOA, GPIO_Pin_0);
}
// 控制函数示例
void LED_On(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_0); } // 低电平亮
void LED_Off(void) { GPIO_SetBits(GPIOA, GPIO_Pin_0); } // 高电平灭
2. 定时器中断 (TIM2)
任务:配置 TIM2 每 500ms 触发一次中断,在中断里翻转 LED。
计算公式:$\text{时间} = \frac{(\text{ARR}+1) \times (\text{PSC}+1)}{\text{时钟频率}}$
(假设总线频率为 72MHz)
核心代码 timer.c
#include "ch32v30x.h"
// 1. 定时器初始化函数
// arr: 自动重装值 (Period), psc: 预分频系数 (Prescaler)
void TIM2_Init(u16 arr, u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure = {0};
NVIC_InitTypeDef NVIC_InitStructure = {0};
// A. 开启 TIM2 时钟 (挂载在 APB1 总线上)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// B. 配置定时器参数
TIM_TimeBaseStructure.TIM_Period = arr; // 周期 (计数上限)
TIM_TimeBaseStructure.TIM_Prescaler = psc; // 分频 (计数快慢)
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// C. 开启更新中断 (溢出中断)
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
// D. 配置中断优先级 (NVIC/PFIC)
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; // 中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// E. 启动定时器
TIM_Cmd(TIM2, ENABLE);
}
// 2. 中断服务函数
// 函数名必须查 startup_ch32v30x.S 文件,不能乱写!
// WCH-Interrupt-fast 是 RISC-V 特有的加速修饰符
void TIM2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void TIM2_IRQHandler(void)
{
// 检查是不是 TIM2 的更新中断
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
// --- 业务逻辑:翻转 PA0 ---
if(GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_0) == 0)
GPIO_SetBits(GPIOA, GPIO_Pin_0);
else
GPIO_ResetBits(GPIOA, GPIO_Pin_0);
// --- 关键:清除中断标志位 ---
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
3. 串口通信 (USART1) + printf
任务:配置 USART1 (TX=PA9, RX=PA10) 波特率 115200,并支持 printf。
核心代码 usart.c
#include "ch32v30x.h"
#include <stdio.h>
void USART1_Init(u32 baudrate)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
USART_InitTypeDef USART_InitStructure = {0};
// 1. 开启时钟:同时开启 GPIOA 和 USART1
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
// 2. 配置 TX (PA9) -> 复用推挽输出 (AF_PP)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽!
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 配置 RX (PA10) -> 浮空输入 (IN_FLOATING)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 4. 配置串口参数
USART_InitStructure.USART_BaudRate = baudrate;
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);
// 5. 开启串口
USART_Cmd(USART1, ENABLE);
}
// --- 重定向 printf (如果在 MRS 官方工程里,通常在 debug.c 中已实现) ---
// 如果没有,添加以下代码以支持 printf 输出到 USART1
/*
int _write(int fd, char *buf, int size)
{
int i;
for(i = 0; i < size; i++)
{
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); // 等待发送完成
USART_SendData(USART1, *buf++);
}
return size;
}
*/
4. 主函数 (Main.c) 整合
#include "debug.h"
// 引入上述 .h 文件
int main(void)
{
// 1. 系统优先级分组 (RISC-V/STM32 必需)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 2. 基础初始化
SystemCoreClockUpdate(); // 更新时钟频率变量
Delay_Init(); // 沁恒自带的延时初始化
// 3. 模块初始化
USART1_Init(115200); // 串口波特率
LED_Init(); // LED
printf("System Init OK! Clock: %d Hz\r\n", SystemCoreClock);
// 4. 启动定时器: 500ms
// 72MHz / 7200 = 10kHz (0.1ms)
// 5000 * 0.1ms = 500ms
TIM2_Init(4999, 7199);
while(1)
{
// 主循环空闲,工作都在 TIM2 中断里完成
// 每隔 2秒 打印一次心跳日志
Delay_Ms(2000);
printf("Main Loop Running...\r\n");
}
}
避坑检查清单 (Checklist)
- 时钟:GPIO 和 USART1 在
APB2,TIM2 在APB1。写错总线,外设不工作。 - 引脚模式:串口 TX 必须是
GPIO_Mode_AF_PP(复用推挽)。 - 中断函数名:必须复制启动文件 (
startup_xxx.S) 里的名字,如TIM2_IRQHandler,写错无法进入中断。 - 清除标志位:中断里必须有
TIM_ClearITPendingBit,否则死循环在中断里。
这份笔记就是你迈向 32位单片机世界 的入场券!把它保存好,这就是你的标准模板库!![]()