news 2026/9/30 5:09:39

基于寄存器或固件库用keil5做led灯的开发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于寄存器或固件库用keil5做led灯的开发

今天来从头记录一下用STM32F103C8T6最小系统板做流水灯的全过程。从Keil环境搭建开始,一路做到寄存器点灯、标准库点灯、逻辑分析仪看波形,最后用HAL库加按键中断控制暂停和恢复。代码给得很全,注释也写得很细,照着做基本能跑通。
第一步:Keil 5 环境搭建

装Keil MDK:下载Keil µVision 5安装包,一路Next装好,装完记得用注册机激活。 装器件包:打开Keil,点击Pack Installer图标,搜索STM32F1,找到Keil.STM32F1xx_DFP安装。没装这个包,新建工程时找不到STM32F103C8这个芯片。 装ST-Link驱动:去ST官网下个ST-Link驱动装上,插上板子能在设备管理器里看到ST-Link就说明OK了。

第二步:寄存器方式点灯(任务一)
2.1 新建工程

新建工程,选芯片STM32F103C8,弹出的Manage Run-Time Environment窗口直接关掉,不用管。
2.2 查手册找寄存器地址

咱们要直接操作内存地址,得先知道GPIO的寄存器在哪儿。查《STM32F103参考手册》:

以PB9为例,它在CRH的bit[7:4]。MODE设10(输出模式,2MHz),CNF设00(通用推挽输出),合起来就是0010

/* ===== 寄存器地址宏定义 ===== */#defineRCC_APB2ENR(*(volatileunsignedint*)0x40021018)// 时钟使能寄存器#defineGPIOA_CRL(*(volatileunsignedint*)0x40010800)// PA配置低寄存器#defineGPIOA_ODR(*(volatileunsignedint*)0x4001080C)// PA输出数据寄存器#defineGPIOB_CRH(*(volatileunsignedint*)0x40010C04)// PB配置高寄存器#defineGPIOB_ODR(*(volatileunsignedint*)0x40010C0C)// PB输出数据寄存器#defineGPIOC_CRH(*(volatileunsignedint*)0x40011004)// PC配置高寄存器#defineGPIOC_ODR(*(volatileunsignedint*)0x4001100C)// PC输出数据寄存器/* ===== 软件延时 ===== */voidDelay_ms(volatileunsignedintt){unsignedinti;while(t--)for(i=0;i<800;i++);// 72MHz下约800次循环约为1ms}/* ===== LED控制函数 ===== *//* 红灯点亮(PA4高电平) */voidA_LED_LIGHT(void){GPIOA_ODR|=(1<<4);// PA4 = 1,红灯亮GPIOB_ODR&=~(1<<9);// PB9 = 0,绿灯灭GPIOC_ODR&=~(1<<15);// PC15 = 0,蓝灯灭}/* 绿灯点亮(PB9高电平) */voidB_LED_LIGHT(void){GPIOA_ODR&=~(1<<4);// PA4 = 0,红灯灭GPIOB_ODR|=(1<<9);// PB9 = 1,绿灯亮GPIOC_ODR&=~(1<<15);// PC15 = 0,蓝灯灭}/* 蓝灯点亮(PC15高电平) */voidC_LED_LIGHT(void){GPIOA_ODR&=~(1<<4);// PA4 = 0,红灯灭GPIOB_ODR&=~(1<<9);// PB9 = 0,绿灯灭GPIOC_ODR|=(1<<15);// PC15 = 1,蓝灯亮}/* ===== 主函数 ===== */intmain(void){/* 1. 开时钟:GPIOA、GPIOB、GPIOC */RCC_APB2ENR|=(1<<2);// 使能GPIOARCC_APB2ENR|=(1<<3);// 使能GPIOBRCC_APB2ENR|=(1<<4);// 使能GPIOC/* 2. 配置为推挽输出 *//* PA4:CRL bit[19:16] = 0001(推挽,10MHz) */GPIOA_CRL&=~(0xF<<16);GPIOA_CRL|=(0x1<<16);/* PB9:CRH bit[7:4] = 0010(推挽,2MHz) */GPIOB_CRH&=~(0xF<<4);GPIOB_CRH|=(0x2<<4);/* PC15:CRH bit[31:28] = 0011(推挽,50MHz) */GPIOC_CRH&=~(0xF<<28);GPIOC_CRH|=(0x3<<28);/* 3. 初始状态全部熄灭 */GPIOA_ODR&=~(1<<4);GPIOB_ODR&=~(1<<9);GPIOC_ODR&=~(1<<15);/* 4. 循环流水灯 */while(1){A_LED_LIGHT();// 红灯亮Delay_ms(1000);// 延时1秒B_LED_LIGHT();// 绿灯亮Delay_ms(1000);C_LED_LIGHT();// 蓝灯亮Delay_ms(1000);}}voidSystemInit(void){}

接着我们可以在源代码基础上,增加pc13亮的一步

在GPIOC_CRH里把PC13也配成推挽输出,位置是bit[23:20]。

增加一个D_LED_LIGHT()函数,让PC13输出高电平。

主循环里加上第四个灯。

/* ===== 寄存器地址宏定义 ===== */#defineRCC_APB2ENR(*(volatileunsignedint*)0x40021018)#defineGPIOA_CRL(*(volatileunsignedint*)0x40010800)#defineGPIOA_ODR(*(volatileunsignedint*)0x4001080C)#defineGPIOB_CRH(*(volatileunsignedint*)0x40010C04)#defineGPIOB_ODR(*(volatileunsignedint*)0x40010C0C)#defineGPIOC_CRH(*(volatileunsignedint*)0x40011004)#defineGPIOC_ODR(*(volatileunsignedint*)0x4001100C)/* ===== 软件延时 ===== */voidDelay_ms(volatileunsignedintt){unsignedinti;while(t--)for(i=0;i<800;i++);}/* ===== LED控制函数 ===== *//* 红灯亮(PA4) */voidA_LED_LIGHT(void){GPIOA_ODR|=(1<<4);// PA4 = 1GPIOB_ODR&=~(1<<9);// PB9 = 0GPIOC_ODR&=~(1<<15);// PC15 = 0GPIOC_ODR&=~(1<<13);// PC13 = 0}/* 绿灯亮(PB9) */voidB_LED_LIGHT(void){GPIOA_ODR&=~(1<<4);// PA4 = 0GPIOB_ODR|=(1<<9);// PB9 = 1GPIOC_ODR&=~(1<<15);// PC15 = 0GPIOC_ODR&=~(1<<13);// PC13 = 0}/* 蓝灯亮(PC15) */voidC_LED_LIGHT(void){GPIOA_ODR&=~(1<<4);// PA4 = 0GPIOB_ODR&=~(1<<9);// PB9 = 0GPIOC_ODR|=(1<<15);// PC15 = 1GPIOC_ODR&=~(1<<13);// PC13 = 0}/* 板载LED亮(PC13) */voidD_LED_LIGHT(void){GPIOA_ODR&=~(1<<4);// PA4 = 0GPIOB_ODR&=~(1<<9);// PB9 = 0GPIOC_ODR&=~(1<<15);// PC15 = 0GPIOC_ODR|=(1<<13);// PC13 = 1}/* ===== 主函数 ===== */intmain(void){/* 1. 开时钟:GPIOA、GPIOB、GPIOC */RCC_APB2ENR|=(1<<2);RCC_APB2ENR|=(1<<3);RCC_APB2ENR|=(1<<4);/* 2. 配置为推挽输出 *//* PA4:CRL bit[19:16] = 0001 */GPIOA_CRL&=~(0xF<<16);GPIOA_CRL|=(0x1<<16);/* PB9:CRH bit[7:4] = 0010 */GPIOB_CRH&=~(0xF<<4);GPIOB_CRH|=(0x2<<4);/* PC15:CRH bit[31:28] = 0011 */GPIOC_CRH&=~(0xF<<28);GPIOC_CRH|=(0x3<<28);/* PC13:CRH bit[23:20] = 0011 */GPIOC_CRH&=~(0xF<<20);GPIOC_CRH|=(0x3<<20);/* 3. 初始状态全部熄灭 */GPIOA_ODR&=~(1<<4);GPIOB_ODR&=~(1<<9);GPIOC_ODR&=~(1<<15);GPIOC_ODR&=~(1<<13);/* 4. 四个灯循环流水 */while(1){A_LED_LIGHT();// 红灯亮Delay_ms(1000);B_LED_LIGHT();// 绿灯亮Delay_ms(1000);C_LED_LIGHT();// 蓝灯亮Delay_ms(1000);D_LED_LIGHT();// 板载LED亮Delay_ms(1000);}}voidSystemInit(void){}

实验任务二

下载 STM32F10x_StdPeriph_Lib_V3.5.0 标准外设库。这个库ST现在不直接提供了,网上搜“STM32F10x标准外设库V3.5.0”能找到很多镜像下载。解压之后你会看到这么几个关键文件夹:Libraries、Project、Utilities。咱们主要用前两个。
先在电脑上找个地方(比如桌面)建一个工程根目录,名字随意,比如叫 LED_StdLib。打开后在里面建几个子文件夹,像这样:
text

LED_StdLib/
├── Start/ // 存放启动文件和CMSIS内核文件
├── Library/ // 存放标准库外设驱动源文件
├── User/ // 存放用户自己的代码(main.c等)
└── Output/ // Keil编译输出文件(自动生成)

往文件夹里拷文件

Start文件夹要放三个东西:

从标准库包的路径 Libraries\CMSIS\CM3\CoreSupport 里,把 core_cm3.h 拷过来。

从 Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x 里,把 system_stm32f10x.c、system_stm32f10x.h、stm32f10x.h 这三个文件拷过来。

从 Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm 里,把启动文件拷过来。STM32F103C8T6属于中等容量(Medium-density),对应的启动文件是 startup_stm32f10x_md.s。

Library文件夹的操作很简单:

把标准库包路径 Libraries\STM32F10x_StdPeriph_Driver 下的 inc 和 src 两个文件夹,整个复制到你的 Library 文件夹里。inc 里是所有外设的头文件(.h),src 里是对应的源文件(.c)。建议全部拷过来,后面用哪个加哪个,省得反复折腾。
Keil里新建工程和分组

打开Keil µVision 5,点 Project → New µVision Project,保存路径选到 User 文件夹,工程名随便取(比如 LED_StdLib),点击保存。

弹出的芯片选择窗口里,选 STMicroelectronics → STM32F1 Series → STM32F103 → STM32F103C8。选完弹出一个 Manage Run-Time Environment 窗口,直接点 Cancel 关掉,咱们手动加文件,不用它在线库。

然后在左侧 Project 窗口里,右键 Target 1,选 Manage Project Items,建三个组:Start、Library、User。建好之后,分别往三个组里添加文件:

Start 组:添加 startup_stm32f10x_md.s、core_cm3.h、system_stm32f10x.c、stm32f10x.h Library 组:从 Library/src 里挑你需要的源文件。点灯实验最少要加 stm32f10x_gpio.c、stm32f10x_rcc.c、misc.c User 组:添加你的 main.c(先建个空的或者直接把之前的代码贴进去)

配置魔术棒

这是最容易出错的一步,很多人编译报错或者串口printf用不了,都是这里没配对。

点魔术棒图标,进入 C/C++ 选项卡:

Define(全局宏定义):填 USE_STDPERIPH_DRIVER。这个宏告诉编译器“我用的标准库”,stm32f10x.h 里会根据这个宏去包含 stm32f10x_conf.h。

Include Paths(头文件路径):把 Start、Library/inc、User 三个文件夹都加进来。少了任何一个,编译都会报“找不到头文件”。

另外,Target 选项卡里,勾上 Use MicroLIB。这个选项跟printf重定向有关,虽然咱们这个实验不一定要用printf,但勾上没坏处,以后加串口调试就省事了。

#include"stm32f10x.h"// 这个头文件已经通过路径和宏定义自动找到voidDelay_ms(unsignedintt){unsignedinti;while(t--)for(i=0;i<800;i++);}voidLED_Init(void){GPIO_InitTypeDef GPIO_InitStructure;/* 开GPIOA/B/C时钟 */RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC,ENABLE);/* 配置PA4推挽输出 */GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);/* 配置PB9推挽输出 */GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;GPIO_Init(GPIOB,&GPIO_InitStructure);/* 配置PC13推挽输出 */GPIO_InitStructure.GPIO_Pin=GPIO_Pin_13;GPIO_Init(GPIOC,&GPIO_InitStructure);/* 初始全部熄灭 */GPIO_ResetBits(GPIOA,GPIO_Pin_4);GPIO_ResetBits(GPIOB,GPIO_Pin_9);GPIO_ResetBits(GPIOC,GPIO_Pin_13);}intmain(void){LED_Init();while(1){GPIO_SetBits(GPIOA,GPIO_Pin_4);GPIO_ResetBits(GPIOB,GPIO_Pin_9);GPIO_ResetBits(GPIOC,GPIO_Pin_13);Delay_ms(1000);GPIO_ResetBits(GPIOA,GPIO_Pin_4);GPIO_SetBits(GPIOB,GPIO_Pin_9);GPIO_ResetBits(GPIOC,GPIO_Pin_13);Delay_ms(1000);GPIO_ResetBits(GPIOA,GPIO_Pin_4);GPIO_ResetBits(GPIOB,GPIO_Pin_9);GPIO_SetBits(GPIOC,GPIO_Pin_13);Delay_ms(1000);}}

任务三环境搭建:HAL库 + STM32CubeMX

任务三其实有两种搭法:纯手工搭HAL库工程,或者用STM32CubeMX生成工程。CubeMX省事得多,下面主要讲CubeMX的方式,最后顺带提一下纯手工的思路。
先装STM32CubeMX

去ST官网下载STM32CubeMX,需要注册账号。安装的时候有个坑:要保证电脑上装了Java运行环境(JRE),CubeMX是基于Java的。装完之后打开,第一次用需要等它下载固件包(Firmware Package),也就是STM32CubeF1,里面包含了HAL库的所有源文件。下载过程可能比较慢,耐心等或者用离线包。
用CubeMX新建工程

打开CubeMX,点 File → New Project,在芯片选择界面搜索框里输入 STM32F103C8,选中 STM32F103C8Tx,点 Start Project。
配置引脚

调试接口:左边 System Core → SYS,Debug选 Serial Wire(也就是SWD),否则烧录一次后芯片可能被锁死。

时钟源:System Core → RCC,High Speed Clock (HSE) 选 Crystal/Ceramic Resonator,表示用外部晶振。

LED引脚:在右边的芯片引脚图上,找到PA4、PB9、PC13,分别左键点击,选择 GPIO_Output。

按键引脚:找到PA0,左键点击,选择 GPIO_EXTI0。注意,选EXTI0之后PA0就变成外部中断模式了,不再是一般的输入输出。
配置时钟树

点上方 Clock Configuration 选项卡,把HSE输入设为8MHz,PLL Source选HSE,PLL倍频系数设为 x9,系统时钟源选 PLLCLK。这样系统时钟就是 8MHz × 9 = 72MHz。AHB和APB2不分频,APB1分频系数选 /2(36MHz)。
配置GPIO详细参数

回到 Pinout & Configuration 选项卡,在左边 System Core → GPIO 里,逐个引脚配置:

PA4、PB9、PC13(LED):Mode选 Output Push Pull,Pull选 No pull-up and no pull-down,Speed选 Low 或 Medium。初始电平可以先设High或Low,代码里后面会改。 PA0(按键):Mode已经是 External Interrupt Mode with Falling edge trigger detection(下降沿触发),Pull选 Pull-up,这样按键另一端接地,平时上拉为高,按下变低,触发下降沿中断。

配置NVIC中断

左边 System Core → NVIC,在 EXTI line0 interrupt 那一行勾上 Enabled。抢占优先级(Preemption Priority)设 2,子优先级(Sub Priority)设 0,随便设一个就行,只要不跟其他中断冲突。
生成工程

点上方 Project Manager 选项卡:

Project Name:随便取,比如 LED_HAL Project Location:选一个没有中文和空格的路径 Toolchain/IDE:选 MDK-ARM V5 Code Generator:勾上 Generate peripheral initialization as a pair of '.c/.h' files per peripheral,这样每个外设的初始化代码会单独放在 gpio.c 里,主文件 main.c 会干净很多

点右上角的 GENERATE CODE,CubeMX就会自动生成完整工程,并弹出“Open Project”按钮,点一下直接用Keil打开。
在Keil里补充用户代码

CubeMX生成的工程在 main.c 里已经写好了所有初始化函数,你只需要在两个固定的 USER CODE BEGIN 和 USER CODE END 标记之间加自己的代码。这些标记是CubeMX保留的,重新生成代码时,标记之外的内容会被覆盖,标记之内的会保留。

全局变量:在 /* USER CODE BEGIN PV/ 和 /USER CODE END PV/ 之间加上:volatile uint8_t run_flag = 1;
中断回调函数:在 /
USER CODE BEGIN 4/ 和 /USER CODE END 4/ 之间加上:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if(GPIO_Pin == GPIO_PIN_0) { run_flag = !run_flag; } }
主循环:在 while(1) 里面,/
USER CODE BEGIN WHILE/ 和 /USER CODE END WHILE */ 之间加上流水灯逻辑:

if(run_flag){HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET);HAL_GPIO_WritePin(GPIOB,GPIO_PIN_9,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_RESET);HAL_Delay(1000);HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOB,GPIO_PIN_9,GPIO_PIN_SET);HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_RESET);HAL_Delay(1000);HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOB,GPIO_PIN_9,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_SET);HAL_Delay(1000);}else{HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOB,GPIO_PIN_9,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_RESET);}
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 5:09:21

Laya模型实战:从零安装到LoRA微调,打造System 1实时决策模型

最近在GitHub上刷到一个叫Laya的项目&#xff0c;star数直接冲到17K&#xff0c;社区里到处是拿它和Jev对比的帖子。我在做实时决策场景的落地&#xff0c;看到"System 1决策"这个关键词就直接点进去了&#xff0c;跑完一轮之后发现这个模型确实有点东西——它在快速…

作者头像 李华
网站建设 2026/9/30 5:08:51

智能体项目LLM Evals实战:RAGAS与LLM-as-judge评估体系落地指南

1. 为什么智能体项目绕不开LLM Evals这道坎做智能体开发的人都有一个共同的体感&#xff1a;Demo跑通只要一个下午&#xff0c;但要让它在生产环境里稳定干活&#xff0c;可能要折腾三个月。这中间的鸿沟&#xff0c;十有八九卡在评估环节。你搭了一个基于RAG的客服智能体&…

作者头像 李华
网站建设 2026/9/30 5:08:47

从NumPy到LLM:程序员如何用矩阵运算理解大语言模型核心原理

1. 从一行 NumPy 说起&#xff1a;为什么程序员该懂点 LLM1.1 一个真实的学习起点我最早接触 NumPy 的时候&#xff0c;纯粹是为了处理一批传感器采集的数据。那时候的想法很简单&#xff1a;Python 的 list 用着挺顺手&#xff0c;为什么还要学一个新库&#xff1f;直到我用 l…

作者头像 李华
网站建设 2026/9/30 5:08:38

RAG文档解析瓶颈突破:Docling结构化解析实战指南

1. 为什么 RAG 的瓶颈从来不在模型&#xff0c;而在文档解析做过 RAG 项目的人都有一个共同体会&#xff1a;模型选型、向量库调参、提示词工程这些环节&#xff0c;折腾几天总能跑通&#xff0c;但真正让整个管线"翻车"的&#xff0c;往往是文档解析这一步。你拿一份…

作者头像 李华
网站建设 2026/9/30 5:08:16

人工智能期末速通:A*搜索、反向传播与大模型复习框架

1. 先搞清楚期末到底考什么&#xff0c;再决定背什么1.1 速通的第一步是画边界&#xff0c;不是打开第一页每到期末&#xff0c;我最怕看到的不是书厚&#xff0c;而是一堆人从第一章第一页开始往下翻。人工智能这门课的特殊性在于&#xff0c;它的知识密度极度不均匀&#xff…

作者头像 李华
网站建设 2026/9/30 5:08:16

AI布线不是替代工程师,而是数据驱动的PCB设计范式升级

1. 这不是“AI替代工程师”&#xff0c;而是布线逻辑的底层重写“规则已死&#xff01;AI 布线终局是数据驱动”——这句话刚看到时&#xff0c;我手边正捏着一份刚被DRC报错27处的四层高速板设计稿。不是没按《高速信号线布线原则》操作&#xff0c;也不是忘了设置Altium里的等…

作者头像 李华