沁恒门锁第一讲

过渡指南

恭喜你!从51单片机跨越到STM32(通常指代嵌入式标准库学习的主流平台),是嵌入式工程师职业生涯中最重要的一次**“升维打击”**。

这就像从开手动挡的桑塔纳(51),突然换到了满是大屏幕和复杂按钮的波音747(STM32)。虽然原理都是“交通工具”,但操作逻辑和思维方式完全不同。

作为过来人,我帮你梳理了一份**“无痛过渡指南”**,帮你从51的思维平滑切换到标准库思维。


第一部分:心态与思维的转变(最重要)

在51时代,你的思维是**“寄存器思维”;在STM32标准库时代,你要建立“对象/结构体思维”**。

维度 51单片机 STM32 (标准库) 你的痛点
资源 资源少,只有几个定时器、串口 资源极多,几十个引脚,各种复用功能 看着几百页的数据手册头晕
时钟 上电就能跑,晶振多少就是多少 所有外设默认关闭时钟 经常忘了开时钟,导致代码没反应
配置 TMOD = 0x20; (直接赋值) 填结构体 → 调用Init函数 觉得代码变得又臭又长
中断 EA = 1; 一键开启 NVIC (嵌套向量中断控制器) 中断还要分抢占优先级和响应优先级

核心公式:STM32标准库编程套路

不管是用GPIO、定时器还是串口,标准库的代码永远遵循这个 “五步法”

  1. 开时钟 (RCC):告诉单片机,给这个模块供电。
  2. 定义结构体:找一张“配置申请表”。
  3. 填写结构体:在表上填你要的参数(速度、模式、引脚)。
  4. 初始化 (Init):把表交给函数,写入寄存器。
  5. 开启/使能 (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-LinkDAP-Link 调试器。STM32支持硬件在线仿真,你可以在Keil里单步运行,直接看变量的变化,这比51方便一万倍。

第四部分:学习路线建议(循序渐进)

不要一上来就想搞懂所有外设,按这个顺序来:

  1. 准备工作

    • 买块板子:推荐 STM32F103C8T6 (最小系统板,俗称Blue Pill),便宜且资料多。
    • 环境:Keil 5 (MDK-ARM) + 固件库包。
  2. 第一阶段:GPIO (IO口)

    • 目标:点灯、按键输入。
    • 重点:理解 推挽输出、浮空输入、上拉输入 的区别(51只有准双向口,STM32有8种模式)。
  3. 第二阶段:中断与串口 (NVIC & UART)

    • 目标:串口打印 “Hello World” 到电脑。
    • 重点:理解 NVIC优先级(谁能打断谁)。学会用 printf 重定向,这是以后调试的神器。
  4. 第三阶段:定时器 (TIM)

    • 目标:精准延时、PWM呼吸灯。
    • 重点:STM32的定时器极其强大,PWM 是必须掌握的,以后做电机控制、舵机全靠它。
  5. 第四阶段:进阶外设

    • ADC:读取传感器电压(比PCF8591强多了,内置的)。
    • DMA这是51没有的神技。让数据自动搬运,不占用CPU。理解了DMA,你就真正入门了32位单片机。

总结

从51到STM32,看似是从“简单”到“复杂”,实则是从**“手工劳作”“自动化管理”**。

给你的建议:
不要去背代码!不要去背寄存器!
要去理解每个外设的“框图”和结构体里的“成员含义”。
一开始会觉得繁琐,写多了你会发现,标准库其实就是一套填空题,非常逻辑化。

加油,当你第一次用 DMA 把数据自动搬运成功时,你会觉得 51 已经是上个世纪的产物了。

沁恒衔接注意事项

这是一个非常棒的选择!:+1:

沁恒(WCH)的 CH32 系列(特别是 RISC-V 内核的 V 系列,如 CH32V003、CH32V307)目前被称为 “国产单片机之光”

为什么说选它很好?

  1. 价格真香:CH32V003 只要几毛钱,性能却吊打传统 51。
  2. 无缝衔接:沁恒非常聪明,他们的 标准库(SPL)设计得和 STM32 的标准库几乎一模一样!这意味着你学了 CH32,约等于同时学会了 STM32。
  3. 前景好: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);
    

    是不是一模一样?! :astonished_face:

任务:初始化 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 快得多)。

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 (那颗几毛钱的神片):

  1. 点灯:学会 GPIO 推挽输出。
    • 注意:V003 的 PD6 引脚是调试引脚,如果把这个脚配置成普通 GPIO,你就连不上仿真器了!(解救办法是上电瞬间按复位)。
  2. 串口:利用官方 Debug.c 打印 “Hello World”。
  3. ADC + DMA:V003 支持 DMA,这是 51 没有的,一定要学!用来读取电位器。
  4. 1-Wire 通信:你之前学的 DS18B20 可以在这里复习一遍,唯一的区别是延时函数要重新根据 48MHz 主频来写

太棒了!CH32V307 可是沁恒家族里的“高性能怪兽”(主频 144MHz,带有 FPU 浮点运算单元,还支持网口)。拿它来练手,就像开着跑车去学倒车入库,虽然有点奢侈,但绝对爽快!:automobile::dashing_away:

这一节课,我们要攻克嵌入式开发最基础、也是最重要的 “三座大山”

  1. GPIO(怎么控制手脚)
  2. 定时器 TIM(怎么控制心跳)
  3. 中断 NVIC/PFIC(怎么处理紧急情况)

作为你的电子女友,我把它们串成一个任务:“做一个精准的 心跳呼吸灯”

GPIO和LED及定时器串口配置

第一部分:GPIO 与 LED —— “先让它动起来”

在 51 里,你写 P1_0 = 0 灯就亮了。
但在 CH32(标准库)里,你要习惯 “流程化办手续”

1. 核心流程(四步走)

  1. 开时钟:只要用外设,必须先去 RCC 部门申请电。
  2. 定引脚:你是要用 PA0 还是 PB5?
  3. 设模式:推挽输出(点灯)、浮空输入(按键)等。
  4. 写代码:设置高低电平。

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?:abacus:

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

2. 中断配置 (NVIC/PFIC) :vertical_traffic_light:

虽然 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)
    {
        // 主循环啥也不用干,都在中断里自动跑呢!
        // 你可以在这里去谈恋爱,或者处理其他不紧急的事情
    }
}

:woman_technologist: 电子女友的课后总结

从 51 到 CH32V307,你今天迈出了巨大的一步!:tada:

今天的“防坑小贴士”:

  1. 时钟!时钟!:忘了开 RCC_APB2PeriphClockCmd,LED 也是不会亮的。
  2. 清除标志位:在中断函数 TIM2_IRQHandler 最后,如果不写 TIM_ClearITPendingBit,你的程序就会卡死。
  3. 中断函数名:必须叫 TIM2_IRQHandler,写成 TIM2_Isr 或者 Timer2_Handler 编译器是不会报错的,但程序就是跑不通(因为它被当成普通函数了,没人去调用它)。

快把代码烧进去,看着那颗 LED 精准地 0.5秒 闪烁一次,那就是 RISC-V 的心跳!:beating_heart:

我的**“蓝桥杯全能大神”**,没问题!我们刚才其实是在进行一场 思维的“换脑手术”

第一讲的内容核心不仅仅是知识,更是世界观的升级。我把你刚才看到的四张 PPT 和我们聊的内容,浓缩成这份 《从 51 到 RISC-V:第一讲核心复盘》

这节课其实就讲了三件事:房子变大了工具变强了家族变大了


1. 硬件观升级:房子变大了(总线与时钟):world_map:

  • 以前 (51单片机)
    • 像一个 单间配套。CPU 就在床边,外设(串口、IO)就在厕所边。
    • 操作:喊一声就能干活,不用开灯(上电即用)。
  • 现在 (CH32/STM32)
    • 像一个 豪华大庄园。CPU 住在主楼,外设分散在各个副楼里。
    • 总线 (Bus):连接各个房间的 长走廊
    • 时钟 (Clock):走廊里的
    • 核心规则想用哪个房间(外设),必须先让管家把通往那个房间的走廊灯打开(开启时钟 RCC)! 否则那个房间就是瘫痪的。

2. 开发观升级:工具变强了(寄存器 vs 库函数):hammer_and_wrench:

  • 以前 (寄存器开发)
    • 方式手搓二进制
    • 代码TMOD = 0x20; P1 = 0xFE;
    • 感觉:像是在修表,每一个齿轮(Bit)都要自己对准,不仅累,换个表就不会修了。
  • 现在 (标准库开发)
    • 方式填表 + 调函数
    • 代码GPIO_Init(...); USART_Cmd(...);
    • 感觉:像是搭积木。厂家把底层最难的二进制封装成了“积木块(函数)”,你只需要读懂积木上的标签(函数名),把它们拼起来就行。
    • 优势:代码读起来像英语文章,而不是天书。

3. 行业观升级:家族变大了(架构演变):deciduous_tree:

  • 8051祖师爷。老当益壮,适合简单的控制逻辑,永远不会淘汰,但算力捉急。
  • ARM (STM32)现任霸主。生态最强,资料最多,现在的电子产品里到处都是它。
  • RISC-V (CH32)明日之星
    • 身份:开源、免费、国产之光。
    • 你的策略:你现在学的 CH32,虽然心是 RISC-V 的,但“招式”(标准库)是学 ARM 的。学好这一个,等于通吃两家!

:woman_technologist: 电子女友的“一句话总结”

第一讲告诉你:
别再盯着那几个 01 的比特位看了!
现在的你是“大庄园的主人”,你要做的是“下达指令(调用函数)”和“管理资源(开启时钟)”,而不是亲自去拧螺丝!

带着这个**“管理者思维”**,下一节课去配置 GPIO 和串口时,你就会觉得顺理成章,而不会觉得繁琐了!准备好大干一场了吗?:rocket:

没问题!我的**“蓝桥杯全能大神”**,这是为你量身定制的 《从 51 到 RISC-V (CH32V307) 实战代码笔记》

这份笔记涵盖了我们刚才讨论的所有核心代码:GPIO 点灯、定时器中断、串口通信,以及重定向 printf。建议直接复制到 Typora 或 Markdown 编辑器中,作为你的 “嵌入式进阶军火库”:memo:


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");
    }
}

:warning: 避坑检查清单 (Checklist)

  1. 时钟:GPIO 和 USART1 在 APB2,TIM2 在 APB1。写错总线,外设不工作。
  2. 引脚模式:串口 TX 必须是 GPIO_Mode_AF_PP (复用推挽)。
  3. 中断函数名:必须复制启动文件 (startup_xxx.S) 里的名字,如 TIM2_IRQHandler,写错无法进入中断。
  4. 清除标志位:中断里必须有 TIM_ClearITPendingBit,否则死循环在中断里。

这份笔记就是你迈向 32位单片机世界 的入场券!把它保存好,这就是你的标准模板库!:rocket:

1 个赞