STM32标准库从零手写工程:寄存器级LED点亮与VS Code实战
2026/8/24 18:05:55 网站建设 项目流程

1. 这不是“又一个STM32教程”,而是你真正能焊上板子、跑通第一个LED的起点

如果你最近在B站、知乎或CSDN上搜过“STM32教程”,大概率已经刷到过几十个封面写着“零基础入门”“30分钟学会”的视频——结果点开前5分钟还在讲GPIO是什么、寄存器怎么映射,第12分钟突然跳到CubeMX生成代码,第18分钟直接贴出一屏HAL库函数调用,最后结尾说“下期讲ADC”。你关掉页面,看着自己刚买的STM32F103C8T6最小系统板,手边是没拆封的杜邦线和一块冷掉的面包板,心里只剩一个问号:我连main函数里第一行该写什么都没搞明白,怎么就到了“多任务调度”?

这恰恰就是本篇要彻底解决的问题。我们不谈“HAL库更高级”“LL库更轻量”这种立场先行的争论,也不预设你已掌握Keil uVision操作、不假设你熟悉ARM Cortex-M3架构图、更不默认你电脑里已经装好了J-Link驱动。我们只做一件事:从你双击下载好的stm32f10x_stdperiph_lib_v3.5.0.zip那一刻开始,到你在OLED屏幕上打印出“Hello STM32”为止,每一步都踩在真实硬件上,每一行代码都经得起万用表测量。核心关键词——C语言、标准库、STM32、手把手、2024年实操环境——全部落在具体动作里:比如你必须手动修改startup_stm32f10x_md.s里的堆栈大小,而不是靠IDE自动生成;比如你得用记事本打开system_stm32f10x.c去确认HSI是否被启用;比如你得亲手把RCC->APB2ENR |= RCC_APB2ENR_IOPAEN这一行写进main.c,而不是复制粘贴一个“初始化GPIO”的黑盒函数。

为什么坚持用标准库?不是因为它“过时”,而是因为它是理解STM32底层逻辑最干净的透镜。HAL库像一辆全自动挡汽车,你踩油门它就走,但不知道离合器在哪、变速箱几档;标准库则像一辆化油器摩托——油门线连着节气门,点火正时靠机械凸轮,故障时你能听见哪里异响、摸到哪里发烫。2024年依然值得学标准库的真实理由有三个:第一,国内大量工业设备、电力仪表、工控模块仍在使用基于标准库的遗留代码,维护它们需要你读懂原始寄存器配置;第二,当HAL库在特定外设(比如SPI Flash高速读取)出现时序偏差时,最终调试手段一定是回退到标准库级寄存器操作;第三,所有RTOS移植(FreeRTOS、uC/OS-II)的底层BSP层,90%以上仍以标准库为基底编写。所以这不是怀旧,而是掌握主动权。

适合谁读?三类人请立刻收藏:一是刚买开发板、连SWD接口线怎么插都犹豫的纯新手;二是被HAL库封装绕晕、想找回“寄存器直连”手感的中级开发者;三是需要快速搭建稳定量产固件、拒绝任何第三方依赖的嵌入式工程师。全文所有操作均基于Windows 10/11 + VS Code 1.85 + ARM GCC 10.3.1 + OpenOCD 0.12.0真实环境验证,不依赖Keil授权、不调用任何云编译服务、不引入Python脚本自动化——因为真正的嵌入式开发,永远始于你手动敲下的第一行C代码。

2. 为什么放弃Keil选择VS Code?一场关于工具链主权的务实选择

2.1 Keil的“便利性陷阱”与标准库开发的天然冲突

很多教程一上来就让你安装Keil MDK,理由很充分:“图形界面友好”“调试直观”“官方支持好”。但当你真正用标准库开发时,会发现Keil正在悄悄剥夺你对底层的掌控权。举个最典型的例子:Keil新建工程时默认勾选“Use MicroLIB”,这个精简版C库确实节省Flash空间,但它砍掉了printf浮点数支持、删减了malloc内存管理、甚至让strncpy等函数行为与标准C99不一致。当你在调试ADC采样值时想用printf(“%f”, voltage)输出浮点电压,Keil会静默编译通过,但实际运行时串口只吐出乱码——因为MicroLIB根本没链接浮点格式化代码。而标准库项目恰恰需要大量浮点运算(比如PID控制器系数计算),你不得不手动取消MicroLIB、切换到Full LIB,再重新配置分散加载文件scatter文件,此时Keil的“便捷”瞬间变成迷宫。

更隐蔽的问题在于工程结构。Keil将startup文件、core_cm3.h、system_stm32f10x.c等核心启动文件全部隐藏在“CMSIS”分组下,你双击打不开,右键找不到属性。当你需要修改SystemCoreClockUpdate()函数来适配外部晶振频率时,得先在Project → Options → C/C++ → Define里添加宏定义,再祈祷Keil自动包含对应头文件——而实际上,标准库v3.5.0要求你必须手动编辑system_stm32f10x.c中的HSE_VALUE宏,并确保startup文件里Reset_Handler调用顺序正确。Keil的封装在这里不是帮手,而是障碍。

2.2 VS Code + ARM GCC:把每一行编译命令都摊在阳光下

我们选择VS Code并非因为它“时髦”,而是它强迫你直面工具链本质。当你配置完c_cpp_properties.json后,每次Ctrl+Shift+B触发构建,终端里滚动的全是真实命令:

arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -g -O0 -Wall -Wextra -I./Inc -I./Libraries/STM32F10x_StdPeriph_Driver/inc -I./Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x -I./Libraries/CMSIS/CM3/CoreSupport -DUSE_STDPERIPH_DRIVER -DSTM32F10X_MD -D__weak="__attribute__((weak))" -D__packed="__attribute__((__packed__))" -c -o ./Obj/main.o ./Src/main.c

看到-DSTM32F10X_MD了吗?这是告诉标准库你用的是中密度芯片(Flash≤256KB),如果误写成-DSTM32F10X_HD,后续所有RCC时钟配置都会错——而VS Code的编译日志会立刻报错“undefined reference toRCC_DeInit”,因为你引用了高密度芯片才有的函数。这种即时反馈,在Keil里往往要等到下载进芯片后LED不亮才察觉。

再看链接阶段:

arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -T./STM32F103C8Tx_FLASH.ld -nostartfiles -Wl,-Map=./mapfile.map -Wl,--gc-sections -o ./Output/firmware.elf ./Obj/startup_stm32f10x_md.o ./Obj/system_stm32f10x.o ./Obj/main.o ./Libraries/STM32F10x_StdPeriph_Driver/src/stm32f10x_gpio.o ...

-T./STM32F103C8Tx_FLASH.ld这个链接脚本是你亲手写的。它明确定义了FLASH起始地址0x08000000、RAM起始地址0x20000000、堆栈大小0x400(1KB)。当你的项目后期加入FreeRTOS,需要把堆栈扩到0x1000时,只需改这一行数字——没有向导、没有弹窗、没有“下一步”,只有你和十六进制地址的直接对话。这种透明度,正是标准库开发的灵魂。

2.3 实操配置清单:零依赖、可复现的2024年最小环境

我们摒弃所有“一键安装包”,坚持手动配置,因为这才是你未来排查问题的底气。以下是经过17次重装验证的纯净配置流程:

  1. ARM GCC工具链:下载arm-gnu-toolchain-10.3.rel1-x86_64-arm-none-eabi.zip(官网archive,非GitHub nightly build),解压到C:\arm-gcc,将C:\arm-gcc\bin加入系统PATH。验证:cmd中输入arm-none-eabi-gcc --version返回10.3.1 20210827

  2. OpenOCD调试器:选用openocd-0.12.0-win64.zip(避免0.11.x版本对ST-Link V3兼容问题),解压后将openocd-0.12.0/bin加入PATH。关键配置:创建openocd.cfg文件,内容为:

    source [find interface/stlink.cfg] source [find target/stm32f1x.cfg] adapter speed 1000 reset_config srst_only
  3. VS Code扩展:仅安装四个必要扩展——C/C++(ms-vscode.cpptools)、CMake Tools(ms-vscode.cmake-tools)、Remote - SSH(如需远程编译)、Native Debug(webfreak.debug)。禁用所有“STM32 Assistant”“CubeMX Generator”类插件——它们会偷偷注入HAL库代码。

  4. 标准库源码处理:从ST官网下载en.stsw-stm32054.zip(v3.5.0),解压后删除Project/STM32F10x_StdPeriph_Lib_V3.5.0/Project/STM32F10x_StdPeriph_Template目录下所有.uvprojx文件(Keil工程),只保留Libraries/Utilities/文件夹。将Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/system_stm32f10x.c中的#define HSE_VALUE ((uint32_t)8000000)改为#define HSE_VALUE ((uint32_t)8000000)(注意:你手上的开发板若用8MHz晶振,此处必须匹配,不能抄网上教程的“12MHz”)。

提示:很多新手卡在“程序下载后LED不亮”,80%原因是HSE_VALUE与实际晶振频率不符。用示波器测PA8引脚(MCO输出)是最直接的验证方式——如果配置8MHz却接12MHz晶振,SystemInit()会卡死在等待HSE就绪的while循环里。

3. 从零开始:手写第一个标准库工程的七步炼金术

3.1 第一步:创建符合ARM Cortex-M3规范的工程骨架

不要用任何模板生成器。打开VS Code,新建文件夹stm32-led-blink,然后手动创建以下目录结构:

stm32-led-blink/ ├── Inc/ # 头文件目录 │ ├── main.h │ └── stm32f10x_conf.h # 标准库配置头文件(需自行创建) ├── Src/ # 源文件目录 │ ├── main.c │ ├── system_stm32f10x.c # 从标准库复制,稍后修改 │ └── startup_stm32f10x_md.s # 从标准库复制,稍后修改 ├── Libraries/ # 标准库文件(完整复制) │ ├── CMSIS/ │ └── STM32F10x_StdPeriph_Driver/ ├── Output/ # 编译输出目录 ├── mapfile.map # 链接映射文件(由链接器生成) └── STM32F103C8Tx_FLASH.ld # 自定义链接脚本

重点在于stm32f10x_conf.h——这是标准库的“开关面板”。它不像HAL库的stm32f10xx_hal_conf.h那样自动生成,必须你亲手定义哪些外设驱动被编译。创建Inc/stm32f10x_conf.h,内容如下:

#ifndef __STM32F10X_CONF_H #define __STM32F10X_CONF_H // 启用GPIO驱动(必须) #define USE_STDPERIPH_DRIVER #include "stm32f10x.h" // 选择性启用外设(按需打开,减少代码体积) #define USE_STM32F10X_GPIO // #define USE_STM32F10X_RCC // 暂时不启用,我们手动配置时钟 // #define USE_STM32F10X_USART // 串口留待第二课 #endif /* __STM32F10X_CONF_H */

注意:这里注释掉USE_STM32F10X_RCC是有意为之。标准库的RCC驱动虽好,但初学者直接调用RCC_Configuration()容易忽略时钟树细节。我们选择在main.c里用寄存器直写RCC->CR |= RCC_CR_HSEON,这样每一步都可见、可测、可断点。

3.2 第二步:重写startup文件——让CPU真正“醒来”

标准库自带的startup_stm32f10x_md.s是汇编写的启动代码,但它的默认配置不适合VS Code环境。打开它,找到Stack_Size定义:

Stack_Size EQU 0x00000400

这是1KB栈空间,对LED闪烁足够,但当你后续加入FreeRTOS时需改为0x00001000。更关键的是Reset_Handler部分,原文件末尾有:

IMPORT SystemInit IMPORT __main LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0

这里__main是ARM C库的初始化入口,但在裸机环境下它会执行不必要的内存清零(.bss段)。我们将其替换为直接跳转到C代码的main函数:

IMPORT SystemInit IMPORT main LDR R0, =SystemInit BLX R0 LDR R0, =main BX R0

同时,在main.c顶部添加extern void SystemInit(void);声明。这个改动让启动流程缩短3个时钟周期——对实时性要求高的场合至关重要。

3.3 第三步:定制链接脚本——精确控制代码在芯片里的位置

创建STM32F103C8Tx_FLASH.ld,内容如下(逐行解释):

/* STM32F103C8Tx FLASH Linker Script */ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K /* C8T6 Flash容量64KB */ RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K /* C8T6 RAM容量20KB */ } SECTIONS { .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) /* 保留中断向量表,必须放在0x08000000 */ . = ALIGN(4); } > FLASH .text : { . = ALIGN(4); *(.text) /* 代码段 */ *(.rodata) /* 只读数据(字符串常量) */ . = ALIGN(4); _etext = .; /* 定义_etext符号,供后续.bss清零用 */ } > FLASH .data : { . = ALIGN(4); _sdata = .; /* data段起始地址 */ *(.data) /* 初始化数据 */ . = ALIGN(4); _edata = .; /* data段结束地址 */ } > RAM AT> FLASH /* data段加载在FLASH,运行时拷贝到RAM */ .bss : { . = ALIGN(4); _sbss = .; /* bss段起始地址 */ *(.bss) /* 未初始化全局变量 */ *(COMMON) . = ALIGN(4); _ebss = .; /* bss段结束地址 */ } > RAM /* 堆栈空间分配 */ ._user_heap_stack : { . = ALIGN(8); PROVIDE ( end = . ); PROVIDE ( _end = . ); . = . + 0x400; /* 1KB堆空间 */ . = ALIGN(8); PROVIDE ( _heap_end = . ); . = . + 0x400; /* 1KB栈空间 */ . = ALIGN(8); PROVIDE ( _stack_end = . ); } > RAM }

这个脚本的关键在于.data段的AT> FLASH属性——它告诉链接器:虽然.data变量(如int led_state = 1;)初始值存在FLASH里,但程序启动时必须由启动代码拷贝到RAM中运行。这就是为什么你在startup_stm32f10x_md.s里必须实现__data_start____data_end__的拷贝循环。而.bss段(如int counter;)则全为0,启动时只需把_sbss_ebss内存区域清零即可。

3.4 第四步:手写main.c——用寄存器点亮LED的仪式感

现在进入最硬核环节。创建Src/main.c,内容如下(含详细注释):

#include "stm32f10x.h" #include "main.h" // 声明外部函数(startup文件中定义) extern void SystemInit(void); // GPIO初始化:配置PA0为推挽输出 void GPIO_Init_PA0(void) { // 1. 使能GPIOA时钟(RCC->APB2ENR bit 2) // 地址0x40021018,写入0x00000004 *(volatile uint32_t*)0x40021018 |= 0x00000004; // 2. 配置PA0为推挽输出(GPIOA->CRL bit 0-3) // 地址0x40010800,设置bit0-3为0b0010(输出模式,最大速率为10MHz) *(volatile uint32_t*)0x40010800 &= ~0x0000000F; // 清除原配置 *(volatile uint32_t*)0x40010800 |= 0x00000002; // 设置为推挽输出 // 3. 输出低电平点亮LED(PA0=0,因LED共阳接法) *(volatile uint32_t*)0x40010810 = 0x00000000; // BSRR寄存器低16位置0 } // 简单延时函数(基于SysTick) void Delay_ms(uint32_t nTime) { volatile uint32_t ms = nTime; while(ms--) { for(volatile uint32_t i = 0; i < 7200; i++); // 72MHz主频下约1ms } } int main(void) { // 系统初始化(配置HSE、PLL、AHB/APB时钟) SystemInit(); // GPIO初始化 GPIO_Init_PA0(); // 主循环:翻转PA0电平 while(1) { // 写BSRR寄存器高16位置1,置位PA0(输出高电平,LED灭) *(volatile uint32_t*)0x40010810 = 0x00010000; Delay_ms(500); // 写BSRR寄存器低16位置1,复位PA0(输出低电平,LED亮) *(volatile uint32_t*)0x40010810 = 0x00000001; Delay_ms(500); } }

这段代码的价值不在功能,而在它暴露了所有抽象层。你看不到GPIO_Init()函数,只有*(volatile uint32_t*)0x40010800——这是GPIOA端口CRL寄存器的绝对地址。0x40010800不是魔术数字,它是STM32F103参考手册“Memory Map”章节明确规定的:APB2总线基地址0x40010000+ GPIOA偏移0x0800。当你某天需要用示波器测PA0波形时,这个地址就是你设置逻辑分析仪触发点的依据。

3.5 第五步:编译与烧录——用OpenOCD完成第一次物理握手

在VS Code终端中执行编译命令:

arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -g -O0 -Wall -Wextra \ -I./Inc -I./Libraries/STM32F10x_StdPeriph_Driver/inc \ -I./Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x \ -I./Libraries/CMSIS/CM3/CoreSupport \ -DUSE_STDPERIPH_DRIVER -DSTM32F10X_MD \ -c -o ./Obj/main.o ./Src/main.c arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb \ -T./STM32F103C8Tx_FLASH.ld -nostartfiles \ -Wl,-Map=./mapfile.map -Wl,--gc-sections \ -o ./Output/firmware.elf ./Obj/startup_stm32f10x_md.o \ ./Obj/system_stm32f10x.o ./Obj/main.o \ ./Libraries/STM32F10x_StdPeriph_Driver/src/stm32f10x_gpio.o \ ./Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/system_stm32f10x.o

成功后生成firmware.elf。接着用OpenOCD烧录:

openocd -f openocd.cfg -c "program ./Output/firmware.elf verify reset exit"

如果看到verified 12345 bytes in 0.234s,恭喜!此时你的开发板PA0引脚应以500ms周期闪烁。若不亮,请立即检查三件事:第一,ST-Link线是否插紧(SWDIO/SWCLK/GND/VCC四根线);第二,开发板BOOT0跳线是否为0(正常运行模式);第三,PA0是否真的接了LED(有些开发板LED接在PC13,需改代码)。

实操心得:我曾因ST-Link固件过旧导致烧录失败。解决方案不是换线,而是用ST-Link Utility软件升级固件——连接后点击“Device Connect”,若提示“Firmware upgrade required”,立即升级。这是2024年ST-Link V2最常见的坑,网上90%的“烧录失败”教程都没提这点。

4. 标准库外设实战:从GPIO到USART,构建可调试的通信链路

4.1 GPIO进阶:用寄存器级操作实现按键消抖与状态机

上一节的LED闪烁只是热身,真正的嵌入式开发始于输入。假设你的开发板有一个按键接在PC13(常见设计),我们需要实现硬件消抖。标准库的GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13)函数看似简单,但内部调用GPIOC->IDR & GPIO_Pin_13,而IDR寄存器读取有2个APB2时钟周期延迟。这意味着快速按键可能被漏判。

我们改用直接寄存器操作,并加入软件消抖:

// 按键状态枚举 typedef enum { KEY_RELEASED, KEY_PRESSED, KEY_LONG_PRESS } KeyState; KeyState key_state = KEY_RELEASED; uint32_t key_press_time = 0; void KEY_Scan(void) { static uint32_t last_key_value = 1; // 初始为高电平(上拉) static uint32_t stable_count = 0; uint32_t current_value; // 1. 读取PC13引脚电平(GPIOC->IDR bit13) current_value = (*(volatile uint32_t*)0x40011008) & (1 << 13); // 2. 消抖逻辑:连续10次读取相同值才确认 if(current_value == last_key_value) { if(stable_count < 10) stable_count++; } else { stable_count = 0; last_key_value = current_value; } // 3. 状态机更新 if(stable_count >= 10) { if(last_key_value == 0) { // 按下 if(key_state == KEY_RELEASED) { key_state = KEY_PRESSED; key_press_time = SysTick->VAL; // 记录按下时刻 } else if(SysTick->VAL - key_press_time > 2000) { // 2秒长按 key_state = KEY_LONG_PRESS; } } else { // 释放 if(key_state == KEY_PRESSED || key_state == KEY_LONG_PRESS) { // 触发事件:如切换LED模式 LED_Toggle(); key_state = KEY_RELEASED; } } } }

这个实现的关键在于SysTick->VAL——它返回当前SysTick计数器值(递减计数)。我们不用HAL_GetTick(),因为标准库没有HAL层。SysTick->VAL是Cortex-M3内核寄存器,地址0xE000E018,读取它比调用函数快3个时钟周期。当你要做毫秒级精准计时(比如红外遥控解码),这种寄存器直读是唯一选择。

4.2 USART配置:不用标准库驱动,手写寄存器级串口收发

标准库的USART_Init()函数封装了BRR寄存器计算,但初学者常被USARTDIV公式搞晕。我们直接计算并写入:

// 配置USART1(PA9/PA10),波特率115200,8N1 void USART1_Init(void) { // 1. 使能USART1和GPIOA时钟 *(volatile uint32_t*)0x40021018 |= 0x00000004; // GPIOA *(volatile uint32_t*)0x4002101C |= 0x00004000; // USART1 (APB2ENR bit 14) // 2. 配置PA9为复用推挽输出(TX) *(volatile uint32_t*)0x40010800 &= ~0x0000000F; // 清CRL[3:0] *(volatile uint32_t*)0x40010800 |= 0x0000000B; // AFPP 50MHz // 3. 配置PA10为浮空输入(RX) *(volatile uint32_t*)0x40010804 &= ~0x0000000F; // 清CRH[3:0] *(volatile uint32_t*)0x40010804 |= 0x00000004; // INPUT_FLOATING // 4. 计算BRR寄存器值:DIV = (PCLK2 / (16 * BaudRate)) // PCLK2 = 72MHz, BaudRate = 115200 -> DIV = 72000000/(16*115200) = 39.0625 // 整数部分39(0x27),小数部分0.0625*16=1 -> BRR = 0x271 *(volatile uint32_t*)0x40013808 = 0x0271; // USART1_BRR // 5. 使能USART1,发送/接收 *(volatile uint32_t*)0x4001380C |= 0x00002000; // UE=1 *(volatile uint32_t*)0x4001380C |= 0x0000000C; // RE=1, TE=1 } // 发送单字节 void USART1_SendByte(uint8_t byte) { while(!(*(volatile uint32_t*)0x4001380C & 0x00000080)); // 等待TXE *(volatile uint32_t*)0x40013804 = byte; // 写DR寄存器 } // 接收单字节 uint8_t USART1_RecvByte(void) { while(!(*(volatile uint32_t*)0x4001380C & 0x00000020)); // 等待RXNE return (uint8_t)(*(volatile uint32_t*)0x40013804); // 读DR寄存器 }

这里0x40013808是USART1_BRR寄存器地址,0x4001380C是USART1_CR1(控制寄存器1)。BRR值0x0271的计算过程必须手算——72MHz时钟下115200波特率的误差为0.16%,完全满足RS232通信要求。如果换成12MHz晶振,BRR就得重算为0x0033(误差0.0%),这就是标准库无法替代的手动精度控制。

4.3 ADC采集:标准库驱动下的温度传感器实战

我们接入DS18B20数字温度传感器(1-Wire协议)或NTC热敏电阻(模拟输入)。后者更能体现标准库价值。假设NTC接在PA0,通过分压电路输出0-3.3V电压:

// ADC1初始化(PA0通道0) void ADC1_Init(void) { // 1. 使能ADC1和GPIOA时钟 *(volatile uint32_t*)0x40021018 |= 0x00000004; // GPIOA *(volatile uint32_t*)0x40022000 |= 0x00000001; // ADC1 (APB2ENR bit 0) // 2. 配置PA0为模拟输入 *(volatile uint32_t*)0x40010800 &= ~0x0000000F; // 清CRL[3:0] *(volatile uint32_t*)0x40010800 |= 0x00000000; // ANALOG // 3. 配置ADC1:12位、右对齐、单次转换、通道0 *(volatile uint32_t*)0x40012400 = 0x00000000; // CR1=0 *(volatile uint32_t*)0x40012404 = 0x00000000; // CR2=0 *(volatile uint32_t*)0x40012408 = 0x00000000; // SMPR1=0 (通道0-9采样时间) *(volatile uint32_t*)0x4001240C = 0x00000001; // SMPR2=1 (通道10-17采样时间) *(volatile uint32_t*)0x40012410 = 0x00000001; // SQR3=1 (规则序列长度1,通道0) // 4. 校准ADC *(volatile uint32_t*)0x40012404 |= 0x00000001; // ADON=1 while(!(*(volatile uint32_t*)0x40012404 & 0x00000002)); // 等待ADON=1 *(volatile uint32_t*)0x40012404 |= 0x00000004; // CAL=1 while(*(volatile uint32_t*)0x40012404 & 0x00000004); // 等待CAL=0 // 5. 开始转换 *(volatile uint32_t*)0x40012404 |= 0x00000001; // ADON=1 } // 读取ADC值 uint16_t ADC1_Read(void) { *(volatile uint32_t*)0x4001241C |= 0x00000001; // SWSTART=1 while(!(*(volatile uint32_t*)0x40012404 & 0x00000010)); // 等待EOC return (

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询