嵌入式作业3.1 GPIO点亮小灯
前置数据(知识):嵌入式笔记3.1 GPIO(mcu 手册)
一、学习CH04示例程序,包括gpio.c和main.c#
1. 硬件分析#
通过查看单片机的接口手册,了解需要用到的 GPIO 端口
AHL-STM32L431接口图(LED三色灯部分):

从上图可以知道 GPIO 端口 PB7、PB8、PB9 分别控制红、绿、蓝色灯珠。
因此在编写程序时,应对对应端口寄存器和时钟控制进行操作。
通过二极管的方向我们可以知道小灯是低电平亮,高电平暗。
2. 工程 GPIO-ASM-STM32L431-20231129 分析#
使用汇编语言编写,作用是使蓝灯亮暗交替
- 重要宏定义
用到的宏定义:端口号地址偏移、引脚模式(方向)、引脚中断类型、引脚外设时钟使能控制基地址(引脚控制寄存器基地址)、GPIO寄存器基地址
//======================================================================
//文件名称:gpio.inc
//功能概要:STM32L432RC GPIO底层驱动构件(汇编)程序头文件
//版权所有:苏州大学嵌入式与物联网研究中心(sumcu.suda.edu.cn)
//更新记录:2019-09-27 V2.0
//======================================================================
//端口号地址偏移量宏定义
.equ PTA_NUM,(0<<8)
.equ PTB_NUM,(1<<8)
.equ PTC_NUM,(2<<8)
.equ PTD_NUM,(3<<8)
.equ PTE_NUM,(4<<8)
.equ PTH_NUM,(7<<8)
//GPIO引脚方向宏定义
.equ GPIO_IN,(0)
.equ GPIO_OUTPUT,(1)
//GPIO引脚中断类型宏定义
.equ RISING_EDGE,(1) //上升沿触发
.equ FALLING_EDGE,(2) //下降沿触发
.equ DOUBLE_EDGE,(3) //双边沿触发
//引脚控制寄存器基地址宏定义(只给出PORTA的引脚控制寄存器PCR0的地址,其他由此计算)
.equ RCC_AHB2ENR_BASE,0x4002104C //RCC->AHB2ENR寄存器基地址
//GPIO寄存器基地址宏定义(只给出PORTA的输出寄存器PDOR的地址,其他由此计算)
.equ GPIO_BASE,0x48000000 //GPIO寄存器基地址
用到的宏定义:指示灯端口及引脚、灯状态
//======================================================================
//文件名称:user.inc
//功能概要:包含工程中用到的头文件
//版权所有:SD-ARM(sumcu.suda.edu.cn)
//版本更新:2019-09-01 V1.0
//======================================================================
.include "gpio.inc"
//指示灯端口及引脚定义
.equ LIGHT_RED,(PTB_NUM|7) //红色RUN灯使用的端口/引脚
.equ LIGHT_GREEN,(PTB_NUM|8) //绿色RUN灯使用的端口/引脚
.equ LIGHT_BLUE,(PTB_NUM|9) //蓝色RUN灯使用的端口/引脚
//灯状态宏定义(灯亮、灯暗对应的物理电平由硬件接法决定)
.equ LIGHT_ON,0 //灯亮
.equ LIGHT_OFF,1 //灯暗
//串口宏定义
//用户串口:用三芯TTL-USB串口线(接插线):黑线-地,接着是白线(TX),随后是绿线(RX)
.equ UART_User, 2 //TX引脚:GEC_10;RX引脚:GEC_8(实物板上标识UART0)
//宏定义相关数据
.equ DELAY_NUM,1000000 //延时数(约1秒),控制小灯闪烁频率
.equ UART_BAUD,115200 //串口波特率
//myprintf重定义
.equ printf, myprintf
//定义原中断处理程序名【20200317】
.type USART2_IRQHandler, function
.global USART2_IRQHandler
//定义新中断处理程序名【20200317】
.type UART_User_Handler, function
.global UART_User_Handler
//建立新旧中断处理程序名的映射【20200317】
.equ USART2_IRQHandler,UART_User_Handler
- gpio_port_pin_resolution 函数解读
作用:将参数 “端口号<<8|引脚号” 分离为 “端口号” 和 “引脚号”。
原始寄存器状态:
R0 |
---|
端口号<<8|引脚号 |
函数结束后寄存器状态:
R0 | R1 |
---|---|
端口号 | 引脚号 |
//======================================================================
//文件名称:gpio.s
//功能概要:STM32L432RC GPIO底层驱动构件(汇编)程序文件
//版权所有:苏州大学嵌入式与物联网研究中心(sumcu.suda.edu.cn)
//更新记录:2019-09-27 V2.0
//======================================================================
...
//======================================================================
//函数名称:gpio_port_pin_resolution
//函数返回:无
//参数说明:r0:端口号|引脚号,例:(PTB_NUM|(5u))表示B口5脚,头文件中有宏定义
//功能概要:将传进参数r0进行解析,得出具体端口号与引脚号(如:PORTB|(5)
// 解析为PORTB与5,并将其分别赋值给r0与r1)。
//======================================================================
gpio_port_pin_resolution:
//(1)保存现场
push {lr} //lr进栈(lr中为进入中断前pc的值)
//(2)解析入口参数r0:端口号|引脚号,得到具体端口号和引脚号,并将其分别赋值给r0与r1
mov r4,r0 //r4=r0=端口号|引脚号
mov r5,r0 //r5=r0=端口号|引脚号
lsr r4,#8 //逻辑右移获得端口号,r4=端口号
mov r0,r4 //r0=r4=端口号
mov r6,#0x000000ff
and r5,r6 //r5=引脚号
mov r1,r5 //r1=r5=引脚号
//(3)恢复现场
pop {pc} //恢复现场,lr出栈到pc(即子程序返回)
- gpio_init 函数分析
作用:初始化指定端口引脚作为GPIO引脚功能,并定义为输入或输出。若是输出,还指定初始状态是低电平或高电平。
步骤:
- 保存现场
- 得到需要操作的端口号和引脚号
- 启用对应端口外设时钟控制
- 设置引脚模式
- 设置端口引脚初始状态(电平)
参数寄存器状态:
R0 | R1 | R2 |
---|---|---|
端口号<<8|引脚号 | 引脚模式(方向) | 端口引脚初始状态 |
- 保存现场
//======================================================================
//文件名称:gpio.s
//功能概要:STM32L432RC GPIO底层驱动构件(汇编)程序文件
//版权所有:苏州大学嵌入式与物联网研究中心(sumcu.suda.edu.cn)
//更新记录:2019-09-27 V2.0
//======================================================================
...
//======================================================================
// 函数名称:gpio_init
// 函数返回:无
// 参数说明:r0:(端口号|(引脚号)),例:(PTB_NUM|(5u))表示B口5脚,头文件中有宏定义
// r1:引脚方向(0=输入,1=输出,可用引脚方向宏定义)
// r2:端口引脚初始状态(0=低电平,1=高电平)
// 功能概要:初始化指定端口引脚作为GPIO引脚功能,并定义为输入或输出。若是输出,
// 还指定初始状态是低电平或高电平
// 备 注:端口x的每个引脚控制寄存器PORTx_PCRn的地址=PORT_PCR_BASE+x*0x1000+n*4
// 其中:x=0~4,对应A~E;n=0~31
//======================================================================
.type gpio_init function //声明gpio_init为函数类型
.global gpio_init //将gpio_init定义成全局函数,便于芯片初始化之后调用
gpio_init:
//(1)保存现场
push {r0-r7,lr} //保存现场,pc(lr)入栈
//将入口参数r1、r2转存至r2、r3,预留出r1保存引脚号
mov r3,r2 //r3=r2=端口引脚初始状态
mov r2,r1 //r2=r1=引脚方向
寄存器状态:
R0 | R1 | R2 | R3 |
---|---|---|---|
端口号<<8|引脚号 | 引脚模式(方向) | 引脚模式(方向) | 端口引脚初始状态 |
- 得到需要操作的端口号和引脚号
//(2)调用内部函数,从入口参数r0中解析出端口号引脚号,分别放在r0和r1中
bl gpio_port_pin_resolution //调用内部解析函数,r0=端口号,r1=引脚号
寄存器状态:
R0 | R1 | R2 | R3 |
---|---|---|---|
端口号 | 引脚号 | 引脚模式(方向) | 端口引脚初始状态 |
- 启用对应端口外设时钟控制
//(3)获得待操作端口时钟的RCC->AHB2ENR寄存器的地址
ldr r7,=RCC_AHB2ENR_BASE //r7=RCC->AHB2ENR寄存器基地址
ldr r5,[r7] //r5=RCC->AHB2ENR寄存器中的内容
mov r6,#1 //r6=1
lsl r6,r6,r0 //r6=待操作RCC->AHB2ENR掩码(为1的位由r1决定)
orr r5,r6 //或运算设GPIOxEN=1,GPIOx时钟使能
str r5,[r7] //将r5中的GPIOxEN值更新到RCC->AHB2ENR寄存器中
- 设置引脚模式
操作:
- 得到某GPIO端口基地址(端口模式寄存器的基地址)= GPIOA基地址 + (端口号 * 0x400)
- 得到对应引脚号在端口模式寄存器中需要设置的位置(掩码)= 0b11 << (引脚号 * 2)
- 使用掩码取反再和端口模式寄存器中的原始内容相与是对应引脚的模式位清零
- 根据初始化的需求对端口模式寄存器对应引脚进行设置
//(4)获得待操作端口的GPIO寄存器的地址
mov r7,r0 //r7=r0=端口号
ldr r4,=0x400 //r4=各端口基地址差值(0x400)
mul r7,r7,r4 //r7=待操作端口与A口的偏移地址
ldr r4,=GPIO_BASE //r4=端口GPIOA基地址(即GPIO_BASE)
add r7,r4 //r7=待操作端口GPIO寄存器的地址
mov r4,#2 //r4=2
mov r6,r1 //r6=r1=引脚号
mul r6,r6,r4 //r6=2*r1=待操作引脚左移量(GPIO_MODER_MODE_Pos)
mov r4,#3 //r4=3=GPIO_MODER_MODE_Msk
lsl r4,r6 //r4=待操作GPIO_MODER补码(为1的位由r1决定)
mvn r4,r4 //r4取反
ldr r5,[r7] //r5=GPIO_MODER寄存器中的内容
and r5,r4 //与运算设~GPIOx_MODER,GPIOx_MODER清0
//(5)根据入口参数r2设定引脚输入输出状态
cmp r2,#1 //判断入口参数r2的值
bne gpio_init_2 //若要设置为输入,转到gpio_init_2,将GPIOx_MODER相应位为00
//(5.1)若要设置为输出,继续执行,将GPIOx_MODER相应位为01
mov r4,#1 //r4=2
lsl r4,r4,r6 //r4=待操作GPIO_MODER掩码(为1的位由r1决定)
orr r5,r4 //或运算设GPIOx_MODER=01,引脚被配置为GPIO输出功能
str r5,[r7] //将r5中的GPIOx_MODER值更新到该寄存器中
...
//(5.2)若要设置为输入,转到gpio_init_2,将GPIOx_MODER相应位为00,引脚被配置为GPIO输入功能
gpio_init_2:
mov r4,#3 //r4=2
lsl r4,r4,r6 //r4=待操作GPIO_MODER掩码(为1的位由r1决定)
bic r5,r5,r4
//and r5,r4 //或运算设GPIOx_MODER=01,引脚被配置为GPIO输出功能
str r5,[r7] //将r5中的GPIOx_MODER值更新到该寄存器中
寄存器状态:
R0 | R1 | R2 | R3 | R7 |
---|---|---|---|---|
端口号 | 引脚号 | 引脚模式(方向) | 端口引脚初始状态 | 待操作端口GPIO寄存器的地址 |
- 设置端口引脚初始状态(电平)
操作:
判断需要初始化的电平,若为高电平则使用对应端口的置位/复位寄存器的置位部分,将对应引脚使用或运算置为1,便完成了对应引脚的置位(高电平);若为低电平则使用对应端口的复位寄存器,将对应引脚使用或运算置为1,便完成了对应引脚的复位(低电平)
cmp r3,#1 //判断引脚初始状态
bne gpio_init_1 //若为低电平,转到gpio_init_1,将BRR相应位置1
//(5.1.1)若为高电平,继续执行,将BSRR相应位置1
mov r5,r7 //r5=r7=待操作端口GPIO寄存器的地址
add r5,#0x18 //r5=待操作端口GPIO->BSRR寄存器的地址
ldr r4,[r5] //r4=待操作端口GPIO->BSRR寄存器中的内容
mov r6,#1 //r6=1
lsl r6,r6,r1 //r6=待操作GPIO_BSRR掩码(为1的位由r1决定)
orr r4,r6 //或运算设GPIOx_BSRR=1
str r4,[r5] //将r4中的GPIOx_BSRR值更新到该寄存器中
bl gpio_init_3 //跳转到gpio_init_2
//(5.1.2)若为低电平,转到gpio_init_1,将BRR相应位置1
gpio_init_1:
mov r5,r7 //r5=r7=待操作端口GPIO寄存器的地址
add r5,#0x28 //r5=待操作端口GPIO->BRR寄存器的地址
ldr r4,[r5] //r4=待操作端口GPIO->BRR寄存器中的内容
mov r6,#1 //r6=1
lsl r6,r6,r1 //r6=待操作GPIO_BRR掩码(为1的位由r1决定)
orr r4,r6 //或运算设GPIOx_BRR=1
str r4,[r5] //将r4中的GPIOx_BRR值更新到该寄存器中
bl gpio_init_3
- gpio_set 函数分析
操作与设置端口引脚初始状态(电平)类似
也是通过置位/复位寄存器或复位寄存器对对应端口的对应引脚进行设置。
参数寄存器:
R0 | R1 |
---|---|
端口号<<8|引脚号 | 需要设置的端口引脚状态 |
完成寄存器状态:
R0 | R1 | R3 | R6 |
---|---|---|---|
端口号 | 引脚号 | 需要设置的端口引脚状态 | 待操作端口GPIO寄存器的地址 |
//======================================================================
//文件名称:gpio.s
//功能概要:STM32L432RC GPIO底层驱动构件(汇编)程序文件
//版权所有:苏州大学嵌入式与物联网研究中心(sumcu.suda.edu.cn)
//更新记录:2019-09-27 V2.0
//======================================================================
...
//=====================================================================
//函数名称:gpio_set
//函数返回:无
// 参数说明:r0:(端口号|(引脚号)),例:(PTB_NUM|(5u))表示B口5脚,头文件中有宏定义
// r1:希望设置的端口引脚状态(0=低电平,1=高电平)
//功能概要:当指定引脚被定义为GPIO功能且为输出时,本函数设定引脚状态
// 备 注:端口x的每个引脚控制寄存器PORTx_PCRn的地址=PORT_PCR_BASE+x*0x1000+n*4
// 其中:x=0~4,对应A~E;n=0~31
//=====================================================================
.type gpio_set function //声明gpio_set为函数类型
.global gpio_set //将gpio_set定义成全局函数,便于芯片初始化之后调用
gpio_set:
//(1)保存现场
push {r0-r7,lr} //保存现场,pc(lr)入栈
//将入口参数r1转存至r3,预留出r1保存引脚号
mov r3,r1 //r3=r1=希望设置的端口引脚状态
//------------------------------------------------------------------------
//(2)从入口参数r0中解析出端口号引脚号,分别放在r0和r1中
bl gpio_port_pin_resolution //调用内部函数,r0=端口号,r1=引脚号
mov r5,r0 //r5=r0=端口号
ldr r6,=0x400 //r6=各GPIO口的基地址差值(400h)
mul r6,r6,r5 //r6=待操作GPIO口与GPIOA的地址偏移
//(3)根据入口参数r3,设定引脚状态(0=低电平,1=高电平)
cmp r3,#1 //判断引脚初始状态
bne gpio_set_1 //若为低电平,转到gpio_set_1,将BRR相应位置1
//(3.1)若为低电平,继续执行,将BSRR相应位置1
ldr r5,=GPIO_BASE+0x18 //r5=第一个端口GPIO->BSRR寄存器的地址
add r5,r6 //r5=待操作端口GPIO->BSRR寄存器的地址
ldr r4,[r5] //r4=待操作端口GPIO->BSRR寄存器中的内容
mov r6,#1 //r6=1
lsl r6,r6,r1 //r6=待操作GPIO_BSRR掩码(为1的位由r1决定)
orr r4,r6 //或运算设GPIOx_BSRR=1
str r4,[r5] //将r4中的GPIOx_BSRR值更新到该寄存器中
bl gpio_set_2 //跳转到gpio_set_2
//(3.2)若为低电平,转到gpio_set_1,将GPIOx_BRR相应位置1
gpio_set_1:
ldr r5,=GPIO_BASE+0x28 //r5=第一个端口GPIO->BRR寄存器的地址
add r5,r6 //r5=待操作端口GPIO->BRR寄存器的地址
ldr r4,[r5] //r4=待操作端口GPIO->BRR寄存器中的内容
mov r6,#1 //r6=1
lsl r6,r6,r1 //r6=待操作GPIO_BRR掩码(为1的位由r1决定)
orr r4,r6 //或运算设GPIOx_BRR=1
str r4,[r5] //将r4中的GPIOx_BRR值更新到该寄存器中
gpio_set_2:
//------------------------------------------------------------------------
//(4)恢复现场
pop {r0-r7,pc} //恢复现场,lr出栈到pc(即子程序返回)
- main.s 部分分析
主要步骤:
- 调用GPIO初始化函数对GPIO端口引脚进行初始化。
- 使小灯亮暗交替循环,亮暗交替中停留一段时间,并打印提示信息,更改小灯状态用gpio_set函数实现。
//--------------------------------------------------------------------
//主函数,一般情况下可以认为程序从此开始运行(实际上有启动过程,参见书稿)
main:
//(1)======启动部分(开头)主循环前的初始化工作======================
//(1.1)声明main函数使用的局部变量
//(1.2)【不变】关总中断
cpsid i
//(1.3)给主函数使用的局部变量赋初值
//(1.4)给全局变量赋初值
//(1.5)用户外设模块初始化
// 初始化蓝灯, r0、r1、r2是gpio_init的入口参数
ldr r0,=LIGHT_BLUE //r0指明端口和引脚(用=,因常量>=256,需用ldr)
movs r1,#GPIO_OUTPUT //r1指明引脚方向为输出
movs r2,#LIGHT_OFF //r2指明引脚的初始状态为亮
bl gpio_init //调用gpio初始化函数
// 初始化串口UART_User1
movs r0,#UART_User //串口号
ldr r1,=UART_BAUD //波特率
bl uart_init //调用uart初始化函数
//(1.6)使能模块中断
movs r0,#UART_User //串口号
bl uart_enable_re_int //调用uart中断使能函数
//(1.7)【不变】开总中断
cpsie i
//显示hello_information定义的字符串
ldr r0,=hello_information //待显示字符串首地址
bl printf //调用printf显示字符串
//bl . //在此打桩(.表示当前地址),理解发光二极管为何亮起来了?
//(1)======启动部分(结尾)=======================================
//(2)======主循环部分(开头)=====================================
main_loop: //主循环标签(开头)
//(2.1)主循环次数变量mMainLoopCount+1
ldr r2,=mMainLoopCount //r2←变量mMainLoopCount的地址
ldr r1, [r2] //r1←变量mMainLoopCount的内容
movs r3,#1 //r3←1
add r1,r3 //变量+1
str r1,[r2] //放回地址中
//(2.2)未达到主循环次数设定值,继续循环
ldr r2,=MainLoopNUM
cmp r1,r2
blO main_loop //未达到,继续循环
//(2.3)达到主循环次数设定值,执行下列语句,进行灯的亮暗处理
//(2.3.1)清除循环次数变量
ldr r2,=mMainLoopCount //r2←mMainLoopCount的地址
movs r1,#0
str r1,[r2]
//(2.3.2)如灯状态标志mFlag为'L',灯的闪烁次数+1并显示,改变灯状态及标志
//判断灯的状态标志
ldr r2,=mFlag
ldr r6,[r2]
cmp r6,#'L'
bne main_light_off //mFlag不等于'L'转
//mFlag等于'L'情况
ldr r3,=mLightCount //灯的闪烁次数mLightCount+1
ldr r1,[r3]
movs r4,#1
add r1,r4
str r1,[r3]
ldr r0,=light_show3 //显示“灯的闪烁次数mLightCount=”
bl printf
ldr r0,=data_format //显示灯的闪烁次数值
ldr r2,=mLightCount
ldr r1,[r2]
bl printf
ldr r2,=mFlag //灯的状态标志改为'A'
movs r7,#'A'
str r7,[r2]
ldr r0,=LIGHT_BLUE //亮灯
ldr r1,=LIGHT_ON
bl gpio_set
ldr r0, =light_show1 //显示灯亮提示
bl printf
//mFlag等于'L'情况处理完毕,转
b main_exit
//(2.3.3)如灯状态标志mFlag为'A',改变灯状态及标志
main_light_off:
ldr r2,=mFlag //灯的状态标志改为'L'
movs r7,#'L'
str r7,[r2]
ldr r0,=LIGHT_BLUE //暗灯
ldr r1,=LIGHT_OFF
bl gpio_set
ldr r0, =light_show2 //显示灯暗提示
bl printf
main_exit:
b main_loop //继续循环
//(2)======主循环部分(结尾)=====================================
3. 工程 GPIO-Output-Component_STM32L431_20200928 分析#
本工程使用C语言编写,与上面汇编工程一样是使蓝灯亮暗交替,其函数的实现也与汇编的实现类似。
主要用到的函数有:main、gpio_get_port_pin、gpio_init、gpio_set
- 一些重要的宏(gpio.h和user.h)
//===========================================================================
//文件名称:gpio.h
//功能概要:GPIO底层驱动构件头文件
//版权所有:SD-Arm(sumcu.suda.edu.cn)
//版本更新:20190520-20200221
//芯片类型:STM32
//===========================================================================
#ifndef GPIO_H //防止重复定义(_GPIO_H 开头)
#define GPIO_H
#include "mcu.h" //包含公共要素头文件
#include "string.h"
// 端口号地址偏移量宏定义
#define PTA_NUM (0<<8)
#define PTB_NUM (1<<8)
#define PTC_NUM (2<<8)
#define PTD_NUM (3<<8)
#define PTE_NUM (4<<8)
#define PTH_NUM (7<<8)
// GPIO引脚方向宏定义
#define GPIO_INPUT (0) //GPIO输入
#define GPIO_OUTPUT (1) //GPIO输出
// GPIO引脚拉高低状态宏定义
#define PULL_UP (0x01u) //拉高
#define PULL_DOWN (0x02u) //拉低
// GPIO引脚中断类型宏定义
#define RISING_EDGE (1) //上升沿触发
#define FALLING_EDGE (2) //下降沿触发
#define DOUBLE_EDGE (3) //双边沿触发
// GPIO输出速率宏定义
#define LOW_SPEED (0x00u) //低速
#define MSDIUM_SPEED (0x01u) //中速
#define HIGH_SPEED (0x02u) //高速
#define VERY_HIGH_SPEED (0x03u) //超高速
//======================================================================
//文件名称:user.h(user头文件)
//制作单位:SD-Arm(sumcu.suda.edu.cn)
//更新记录:20181201-20200830
//概要说明:(1)包含用到的头文件,为达到应用程序的可移植性,具体硬件要接
// 芯片哪个引脚,需要在这里宏定义,目的是实现对具体硬件对象
// 编程,而不是对芯片的引脚编程
// (2)中断处理程序名字在此宏定义,以便isr.c可移植
//======================================================================
#ifndef USER_H //防止重复定义(USER_H 开头)
#define USER_H
//(1)【固定】文件包含
#include "printf.h"
#include "gpio.h"
#include "gec.h"
//(2)【变动】指示灯端口及引脚定义—根据实际使用的引脚改动
//指示灯端口及引脚定义
#define LIGHT_RED (PTB_NUM|7) //红灯,(GEC_56)
#define LIGHT_GREEN (PTB_NUM|8) //绿灯,(GEC_55)
#define LIGHT_BLUE (PTB_NUM|9) //蓝灯,(GEC_54)
//灯状态宏定义(灯亮、灯暗对应的物理电平由硬件接法决定)
#define LIGHT_ON 0 //灯亮
#define LIGHT_OFF 1 //灯暗
//(3)【变动】UART可用模块定义
#define UART_Debug UART_3 //用于程序更新,无法被使用
#define UART_User UART_2 //用户串口(黑-GND;白-TX;绿-RX)
//(4)【变动】中断服务函数宏定义
#define UART_User_Handler USART2_IRQHandler //用户串口中断函数
#endif //防止重复定义(USER_H 结尾)
- gpio.c前置数组分析
//===========================================================================
//文件名称:gpio.c
//功能概要:GPIO底层驱动构件源文件
//版权所有:SD-Arm(sumcu.suda.edu.cn)
//版本更新:20181201-20200221
//芯片类型:STM32
//===========================================================================
#include "gpio.h"
//GPIO口基地址放入常数数据组GPIO_ARR[0]~GPIO_ARR[5]中
GPIO_TypeDef * GPIO_ARR[] =
{(GPIO_TypeDef *)GPIOA_BASE,(GPIO_TypeDef *)GPIOB_BASE,
(GPIO_TypeDef *)GPIOC_BASE,(GPIO_TypeDef *)GPIOD_BASE,
(GPIO_TypeDef *)GPIOE_BASE,(GPIO_TypeDef *)GPIOH_BASE};
//====定义扩展中断IRQ号对应表====
IRQn_Type table_irq_exti[7] = {EXTI0_IRQn, EXTI1_IRQn, EXTI2_IRQn,
EXTI3_IRQn, EXTI4_IRQn, EXTI9_5_IRQn, EXTI15_10_IRQn};
IRQn_Type table_irq_exti暂时还用不到,先不分析
GPIO_TypeDef:
作用:保存GPIO端口寄存器(地址)内容的结构体。
其中每个元素都表示具体寄存器中的内容,例如:
GPIO_TypeDef * GPIOB_BASE = 0x48000400
那么GPIOB_BASE -> BSRR
就会是GPIOB端口的置位/复位寄存器的内容,因为BSRR
是结构GPIO_TypeDef
的第 7 个32 位无符号的元素,那么&(GPIOB_BASE -> BSRR)
就会等于GPIOB_BASE + (6 * 0x04)
定义位置:stm32l431xx.h

GPIOx:
作用:声明GPIOx端口外设。
定义位置:stm32l431xx.h

GPIOx_BASE:
作用:GPIOx端口的基地址。
定义位置:stm32l431xx.h

AHB2PERIPH_BASE:
作用:AHB2总线外设基地址。
定义位置:stm32l431xx.h

PERIPH_BASE:
作用:外设基地址。
定义位置:stm32l431xx.h

- gpio_get_port_pin 分析
与汇编类似用于将端口号和引脚号分离
//=====================================================================
//函数名称:gpio_get_port_pin
//函数返回:无
//参数说明:port_pin:端口号|引脚号(如:(PTB_NUM)|(9) 表示为B口9号脚)
// port:端口号(传指带出参数)
// pin:引脚号(0~15,实际取值由芯片的物理引脚决定)(传指带出参数)
//功能概要:将传进参数port_pin进行解析,得出具体端口号与引脚号,分别赋值给port与pin,返回。
// (例:(PTB_NUM)|(9)解析为PORTB与9,并将其分别赋值给port与pin)。
//=====================================================================
void gpio_get_port_pin(uint16_t port_pin,uint8_t* port,uint8_t* pin)
{
*port = (port_pin>>8);
*pin = port_pin;
}
- gpio_init 分析
操作与汇编类似:
- 得到对应端口号和引脚号(调用gpio_get_port_pin)
- 使能相应GPIO端口外设时钟
- 设置对应端口引脚模式
- 设定引脚初始状态(调用gpio_set)
//=====================================================================
//函数名称:gpio_init
//函数返回:无
//参数说明:port_pin:(端口号)|(引脚号)(如:(PTB_NUM)|(9) 表示为B口9号脚)
// dir:引脚方向(0=输入,1=输出,可用引脚方向宏定义)
// state:端口引脚初始状态(0=低电平,1=高电平)
//功能概要:初始化指定端口引脚作为GPIO引脚功能,并定义为输入或输出,若是输出,
// 还指定初始状态是低电平或高电平
//=====================================================================
void gpio_init(uint16_t port_pin, uint8_t dir, uint8_t state)
{
GPIO_TypeDef *gpio_ptr; //声明gpio_ptr为GPIO结构体类型指针
uint8_t port,pin; //声明端口port、引脚pin变量
uint32_t temp; //临时存放寄存器里的值
//根据带入参数port_pin,解析出端口与引脚分别赋给port,pin
gpio_get_port_pin(port_pin,&port,&pin);
//根据port,给局部变量gpio_ptr赋值(GPIO基地址)
if(7 == port) //GPIOH
gpio_ptr = GPIO_ARR[port-2];
else
gpio_ptr = GPIO_ARR[port];
//使能相应GPIO时钟
RCC->AHB2ENR |= (RCC_AHB2ENR_GPIOAEN<<(port * 1u));
//清GPIO模式寄存器对应引脚位
temp = gpio_ptr->MODER;
temp &= ~(GPIO_MODER_MODE0 << (pin * 2u));
if(dir == 1) //定义为输出引脚
{
temp |= (GPIO_OUTPUT << (pin * 2u));
gpio_ptr->MODER = temp;
gpio_set(port_pin,state); //调用gpio_set函数,设定引脚初始状态
}
else //定义为输入引脚
{
temp |= (GPIO_INPUT << (pin * 2u));
gpio_ptr->MODER = temp;
}
}
RCC_AHB2ENR_GPIOAEN:

RCC:
外设声明(基地址)

RCC_TypeDef:
RCC寄存器(地址)内容的结构体


RCC_BASE:
RCC寄存器基地址

AHB1PERIPH_BASE:
AHB1总线外设基地址
- gpio_set分析
与汇编代码类似的通过对应端口号和引脚号的置位/复位寄存器将引脚状态设置为高电平,通过对应端口号和引脚号的复位寄存器将引脚状态设置为低电平。
//=====================================================================
//函数名称:gpio_set
//函数返回:无
//参数说明:port_pin:(端口号)|(引脚号)(如:(PTB_NUM)|(9) 表示为B口9号脚)
// state:希望设置的端口引脚状态(0=低电平,1=高电平)
//功能概要:当指定端口引脚被定义为GPIO功能且为输出时,本函数设定引脚状态
//=====================================================================
void gpio_set(uint16_t port_pin, uint8_t state)
{
//局部变量声明
GPIO_TypeDef *gpio_ptr; //声明port_ptr为GPIO结构体类型指针(首地址)
uint8_t port,pin; //声明端口port、引脚pin变量
//根据带入参数port_pin,解析出端口与引脚分别赋给port,pin
gpio_get_port_pin(port_pin,&port,&pin);
//根据port,给局部变量gpio_ptr赋值(GPIO基地址)
if(7 == port) //GPIOH
gpio_ptr = GPIO_ARR[port-2];
else
gpio_ptr = GPIO_ARR[port];
//根据state,设置对应引脚状态
if(1 == state) //高电平(该引脚对应置位寄存器置1)
gpio_ptr->BSRR = (uint32_t)(1u<<pin);
else //低电平(该引脚对应重置寄存器置1)
gpio_ptr->BRR = (uint32_t)(1u<<pin);
}
- main分析
循环蓝灯亮暗交替(中间有停顿)
//======================================================================
//文件名称:main.c(应用工程主函数)
//框架提供:SD-Arm(sumcu.suda.edu.cn)
//版本更新:20191108-20200419
//功能描述:见本工程的..\01_Doc\Readme.txt
//移植规则:【固定】
//======================================================================
#define GLOBLE_VAR
#include "includes.h" //包含总头文件
//----------------------------------------------------------------------
//声明使用到的内部函数
//main.c使用的内部函数声明处
//----------------------------------------------------------------------
//主函数,一般情况下可以认为程序从此开始运行(实际上有启动过程,参见书稿)
int main(void)
{
//(1)======启动部分(开头)==========================================
//(1.1)声明main函数使用的局部变量
uint32_t mMainLoopCount; //主循环次数变量
uint8_t mFlag; //灯的状态标志
uint32_t mLightCount; //灯的状态切换次数
//(1.2)【不变】关总中断
DISABLE_INTERRUPTS;
//(1.3)给主函数使用的局部变量赋初值
mMainLoopCount=0; //主循环次数变量
mFlag='A'; //灯的状态标志
mLightCount=0; //灯的闪烁次数
//(1.4)给全局变量赋初值
//(1.5)用户外设模块初始化
gpio_init(LIGHT_BLUE,GPIO_OUTPUT,LIGHT_ON); //初始化蓝灯
//(1.6)使能模块中断
//(1.7)【不变】开总中断
ENABLE_INTERRUPTS;
printf("------------------------------------------------------\n");
printf("金葫芦提示:构件法输出控制小灯亮暗 \n");
printf(" 第一次用构件方法点亮的蓝色发光二极管,\n");
printf(" 这是进行应用编程的第一步,可以在此基础上,\n");
printf(" “照葫芦画瓢”地继续学习实践。\n");
printf(" 例如:改为绿灯;调整闪烁频率等。\n");
printf("------------------------------------------------------\n");
//asm ("bl .");
//for(;;) { } //在此打桩,理解蓝色发光二极管为何亮起来了?
//(1)======启动部分(结尾)==========================================
//(2)======主循环部分(开头)========================================
for(;;) //for(;;)(开头)
{
//(2.1)主循环次数变量+1
mMainLoopCount++;
//(2.2)未达到主循环次数设定值,继续循环
if (mMainLoopCount<=12888999) continue;
//(2.3)达到主循环次数设定值,执行下列语句,进行灯的亮暗处理
//(2.3.1)清除循环次数变量
mMainLoopCount=0;
//(2.3.2)如灯状态标志mFlag为'L',灯的闪烁次数+1并显示,改变灯状态及标志
if (mFlag=='L') //判断灯的状态标志
{
mLightCount++;
printf("灯的闪烁次数 mLightCount = %d\n",mLightCount);
mFlag='A'; //灯的状态标志
gpio_set(LIGHT_BLUE,LIGHT_ON); //灯“亮”
printf(" LIGHT_BLUE:ON--\n"); //串口输出灯的状态
}
//(2.3.3)如灯状态标志mFlag为'A',改变灯状态及标志
else
{
mFlag='L'; //灯的状态标志
gpio_set(LIGHT_BLUE,LIGHT_OFF); //灯“暗”
printf(" LIGHT_BLUE:OFF--\n"); //串口输出灯的状态
}
} //for(;;)结尾
//(2)======主循环部分(结尾)========================================
} //main函数(结尾)
给出 gpio set(LIGHT RED,LIGHT OFF);
语句中LIGHT RED
和LIGHT OFF
的值是多少?贴出每一步的查找截图。
值:LIGHT RED——((1<<8)|9) = 0x100000009;LIGHT OFF——1



4. 工程 GPIO-Output-DirectAddress_STM32L431_20200928 分析#
如果上面的汇编和使用构件的工程都看懂了,看这个就没啥意思啦
//====================================================================
//文件名称:main.c(应用工程主函数)
//框架提供:SD-Arm(sumcu.suda.edu.cn)
//版本更新:2017.08, 2020.05
//功能描述:见本工程的<01_Doc>文件夹下Readme.txt文件
//====================================================================
#define GLOBLE_VAR
#include "includes.h" //包含总头文件
//----------------------------------------------------------------------
//声明使用到的内部函数
//main.c使用的内部函数声明处
//----------------------------------------------------------------------
//主函数,一般情况下可以认为程序从此开始运行(实际上有启动过程见书稿)
int main(void)
{
//(1)======启动部分(开头)==========================================
//(1.1)声明main函数使用的局部变量
uint32_t mMainLoopCount; //主循环使用的记录主循环次数变量
uint8_t mFlag; //主循环使用的临时变量
//(1.2)【不变】关总中断
DISABLE_INTERRUPTS;
//(1.3)给主函数使用的局部变量赋初值
mMainLoopCount = 0; //主循环使用的记录主循环次数变量
mFlag='A'; //主循环使用的临时变量:蓝灯状态标志
//(1.4)给全局变量赋初值
//(1.5)用户外设模块初始化
// B口9脚(蓝灯,低电平点亮)
//(1.5.1)声明变量
volatile uint32_t* RCC_AHB2; //GPIO的B口时钟使能寄存器地址
volatile uint32_t* gpio_ptr; //GPIO的B口基地址
volatile uint32_t* gpio_mode; //引脚模式寄存器地址=口基地址
volatile uint32_t* gpio_bsrr; //置位/复位寄存器地址
volatile uint32_t* gpio_brr; //GPIO位复位寄存器
//(1.5.2)变量赋值
RCC_AHB2=(uint32_t*)0x4002104C; //GPIO的B口时钟使能寄存器地址
gpio_ptr=(uint32_t*)0x48000400; //GPIO的B口基地址
gpio_mode=gpio_ptr; //引脚模式寄存器地址=口基地址
gpio_bsrr=gpio_ptr+6; //置位/复位寄存器地址
gpio_brr=gpio_ptr+10; //GPIO位复位寄存器
//(1.5.3)GPIO初始化
//(1.5.3.1)使能相应GPIOB的时钟
*RCC_AHB2|=(1<<1); //GPIOB的B口时钟使能
//(1.5.3.1)定义B口9脚为输出引脚(令D19、D18=01)方法如下:
*gpio_mode &= ~(3<<18); //0b11111111111100111111111111111111;
*gpio_mode |=(1<<18); //0b00000000000001000000000000000000;
//(思考:为什么这样赋值?答案见本文件末尾注①)
//(1.6)使能模块中断
//(1.7)【不变】开总中断
ENABLE_INTERRUPTS;
printf("-----------------------------------------------------\r\n");
printf("金葫芦提示:直接地址方式进行GPIO输出\r\n");
printf(" 这个编程有点难以看懂,使用构件编程就简单多了,\r\n");
printf(" 但是构件制作要经过这一关,因此,我们把构件制作与\r\n");
printf(" 基于构件的编程分成不同过程。学习嵌入式系统,\r\n");
printf(" 以理解GPIO、UART、定时器、Flash、ADC、...\r\n");
printf(" 知识要素为出发点,学会正确运用构件进行应用编程,\r\n");
printf(" 理解和掌握2~3个简单构件的制作方法即可。\r\n");
printf("----------------------------------------------------\r\n");
//for(;;) { } //在此打桩,理解蓝色发光二极管为何亮起来了?
//(1)======启动部分(结尾)==========================================
//(2)======主循环部分(开头)=========================================
for(;;) //for(;;)(开头)
{
//(2.1)主循环次数+1,并判断是否小于特定常数
mMainLoopCount++; //+1
if (mMainLoopCount<=6556677) continue; //如果小于特定常数,继续循环
//(2.2)主循环次数超过特定常数,灯状态进行切换(这样灯会闪烁)
mMainLoopCount=0; //清主循环次数
//切换灯状态
if (mFlag=='A') //若灯状态标志为'A'
{
*gpio_brr|=(1<<9); //设置灯“亮”
printf("蓝灯:亮\r\n"); //通过调试串口输出灯的状态
mFlag='L'; //改变状态标志
}
else //否则,若灯状态标志不为'A'
{
*gpio_bsrr|=(1<<9); //设置灯“暗”
printf("蓝灯:暗\r\n"); //通过调试串口输出灯的状态
mFlag='A'; //改变状态标志
}
} //for(;;)结尾
//(2)======主循环部分(结尾)========================================
}
5. 用直接地址编程方式,实现红绿蓝三灯轮流闪烁#
- 代码:
代码的话我觉得很简单,就是增加了对端口B引脚7、8的控制,具体操作和单纯控制引脚9一样。
//====================================================================
//文件名称:main.c(应用工程主函数)
//框架提供:SD-Arm(sumcu.suda.edu.cn)
//版本更新:2017.08, 2020.05
//功能描述:见本工程的<01_Doc>文件夹下Readme.txt文件
//====================================================================
#define GLOBLE_VAR
#include "includes.h" //包含总头文件
//----------------------------------------------------------------------
//声明使用到的内部函数
//main.c使用的内部函数声明处
//----------------------------------------------------------------------
//主函数,一般情况下可以认为程序从此开始运行(实际上有启动过程见书稿)
int main(void)
{
//(1)======启动部分(开头)==========================================
//(1.1)声明main函数使用的局部变量
uint32_t mMainLoopCount; //主循环使用的记录主循环次数变量
uint8_t mFlag; //主循环使用的临时变量
uint8_t cFlag; //主循环使用的记录上一次亮灯的颜色
//(1.2)【不变】关总中断
DISABLE_INTERRUPTS;
//(1.3)给主函数使用的局部变量赋初值
mMainLoopCount = 0; //主循环使用的记录主循环次数变量
mFlag='A'; //主循环使用的临时变量:灯状态标志
cFlag='B';
//(1.4)给全局变量赋初值
//(1.5)用户外设模块初始化
// B口7、8、9脚(三色全暗,高电平)
//(1.5.1)声明变量
volatile uint32_t* RCC_AHB2; //GPIO的B口时钟使能寄存器地址
volatile uint32_t* gpio_ptr; //GPIO的B口基地址
volatile uint32_t* gpio_mode; //引脚模式寄存器地址=口基地址
volatile uint32_t* gpio_bsrr; //置位/复位寄存器地址
volatile uint32_t* gpio_brr; //GPIO位复位寄存器
//(1.5.2)变量赋值
RCC_AHB2=(uint32_t*)0x4002104C; //GPIO的B口时钟使能寄存器地址
gpio_ptr=(uint32_t*)0x48000400; //GPIO的B口基地址
gpio_mode=gpio_ptr; //引脚模式寄存器地址=口基地址
gpio_bsrr=gpio_ptr+6; //置位/复位寄存器地址
gpio_brr=gpio_ptr+10; //GPIO位复位寄存器
//(1.5.3)GPIO初始化
//(1.5.3.1)使能相应GPIOB的时钟
*RCC_AHB2|=(1<<1); //GPIOB的B口时钟使能
//(1.5.3.1)定义B口7、8、9脚为输出引脚方法如下:
*gpio_mode &= ~(0b111111<<14); //0b11111111111100000011111111111111;清零
*gpio_mode |=(1<<14); //0b00000000000000000100000000000000;
*gpio_mode |=(1<<16); //0b00000000000000010000000000000000;
*gpio_mode |=(1<<18); //0b00000000000001000000000000000000;
// (1.5.4)初始化三色小灯全暗
*gpio_bsrr &=(7<<7);
//(1.6)使能模块中断
//(1.7)【不变】开总中断
ENABLE_INTERRUPTS;
//(1)======启动部分(结尾)==========================================
//(2)======主循环部分(开头)=========================================
for(;;) //for(;;)(开头)
{
//(2.1)主循环次数+1,并判断是否小于特定常数
mMainLoopCount++; //+1
if (mMainLoopCount<=6556677) continue; //如果小于特定常数,继续循环
//(2.2)主循环次数超过特定常数,灯状态进行切换(这样灯会闪烁)
mMainLoopCount=0; //清主循环次数
//切换灯状态
if (mFlag=='A') //若灯状态标志为'A'
{
if(cFlag=='B')
{
*gpio_brr|=(1<<7); //设置灯“红”
printf("小灯状态:红灯\r\n"); //通过调试串口输出灯的状态
cFlag='R'; //改变颜色标志
mFlag='L'; //改变状态标志
}
else if(cFlag=='R')
{
*gpio_brr|=(1<<8); //设置灯“绿”
printf("小灯状态:绿灯\r\n"); //通过调试串口输出灯的状态
cFlag='G'; //改变颜色标志
mFlag='L'; //改变状态标志
}
else if(cFlag=='G')
{
*gpio_brr|=(1<<9); //设置灯“蓝”
printf("小灯状态:蓝灯\r\n"); //通过调试串口输出灯的状态
cFlag='B'; //改变颜色标志
mFlag='L'; //改变状态标志
}
}
else //否则,若灯状态标志不为'A'
{
*gpio_brr&=~(7<<7); //复位寄存器7、8、9位清空
*gpio_bsrr|=(7<<7); //设置灯“暗”
printf("小灯状态:暗\r\n"); //通过调试串口输出灯的状态
mFlag='A'; //改变状态标志
}
} //for(;;)结尾
//(2)======主循环部分(结尾)========================================
}
- 运行效果:
板子的运行情况在博客上不太好放出来,反正效果就是和运行提示信息中的一样就是了

6. 用调用构件方式,实现红绿蓝的八种组合轮流闪烁#
- 代码:
自认为很简单,把上面的几个样例都看懂了就真的没啥可说的了。
//======================================================================
//文件名称:main.c(应用工程主函数)
//框架提供:SD-Arm(sumcu.suda.edu.cn)
//版本更新:20191108-20200419
//功能描述:见本工程的..\01_Doc\Readme.txt
//移植规则:【固定】
//======================================================================
#define GLOBLE_VAR
#include "includes.h" //包含总头文件
//----------------------------------------------------------------------
//声明使用到的内部函数
//main.c使用的内部函数声明处
//----------------------------------------------------------------------
//主函数,一般情况下可以认为程序从此开始运行(实际上有启动过程,参见书稿)
int main(void)
{
//(1)======启动部分(开头)==========================================
//(1.1)声明main函数使用的局部变量
uint32_t mMainLoopCount; //主循环次数变量
uint8_t mFlag; //灯的状态标志
uint32_t mLightCount; //灯的状态切换次数
uint8_t cFlag; //灯的颜色标志
//(1.2)【不变】关总中断
DISABLE_INTERRUPTS;
//(1.3)给主函数使用的局部变量赋初值
mMainLoopCount=0; //主循环次数变量
mFlag='A'; //灯的状态标志
cFlag=0; //灯的颜色标志
mLightCount=0; //灯的闪烁次数
//(1.4)给全局变量赋初值
//(1.5)用户外设模块初始化
gpio_init(LIGHT_RED,GPIO_OUTPUT,LIGHT_OFF); //初始化蓝灯
gpio_init(LIGHT_GREEN,GPIO_OUTPUT,LIGHT_OFF); //初始化蓝灯
gpio_init(LIGHT_BLUE,GPIO_OUTPUT,LIGHT_OFF); //初始化蓝灯
//(1.6)使能模块中断
//(1.7)【不变】开总中断
ENABLE_INTERRUPTS;
//(1)======启动部分(结尾)==========================================
//(2)======主循环部分(开头)========================================
for(;;) //for(;;)(开头)
{
//(2.1)主循环次数变量+1
mMainLoopCount++;
//(2.2)未达到主循环次数设定值,继续循环
if (mMainLoopCount<=12888999) continue;
//(2.3)达到主循环次数设定值,执行下列语句,进行灯的亮暗处理
//(2.3.1)清除循环次数变量
mMainLoopCount=0;
//(2.3.2)如灯状态标志mFlag为'L',灯的闪烁次数+1并显示,改变灯状态及标志
if (mFlag=='A') //判断灯的状态标志
{
mLightCount++;
printf("\n灯的闪烁次数 mLightCount = %d\n",mLightCount);
mFlag='L'; //灯的状态标志
cFlag=(cFlag % 7) + 1; //计算当前小灯要显示的颜色
if(cFlag == 1)
printf("小灯状态:红色\n");
else if(cFlag == 2)
printf("小灯状态:绿色\n");
else if(cFlag == 3)
printf("小灯状态:黄色\n");
else if(cFlag == 4)
printf("小灯状态:蓝色\n");
else if(cFlag == 5)
printf("小灯状态:紫色\n");
else if(cFlag == 6)
printf("小灯状态:青色\n");
else if(cFlag == 7)
printf("小灯状态:白色\n");
if(cFlag == 1 || cFlag == 3 || cFlag == 5 || cFlag == 7)
{
gpio_set(LIGHT_RED,LIGHT_ON); //红灯“亮”
printf(" LIGHT_RED:ON--\n"); //串口输出灯的状态
}
if(cFlag == 2 || cFlag == 3 || cFlag == 6 || cFlag == 7)
{
gpio_set(LIGHT_GREEN,LIGHT_ON); //绿灯“亮”
printf(" LIGHT_GREEN:ON--\n"); //串口输出灯的状态
}
if(cFlag == 4 || cFlag == 5 || cFlag == 6 || cFlag == 7)
{
gpio_set(LIGHT_BLUE,LIGHT_ON); //蓝灯“亮”
printf(" LIGHT_BLUE:ON--\n"); //串口输出灯的状态
}
}
//(2.3.3)如灯状态标志mFlag为'A',改变灯状态及标志
else
{
mFlag='A'; //灯的状态标志
printf("\n小灯状态:灭\n");
gpio_set(LIGHT_RED,LIGHT_OFF); //红灯“暗”
printf(" LIGHT_RED:OFF--\n"); //串口输出灯的状态
gpio_set(LIGHT_GREEN,LIGHT_OFF); //绿灯“暗”
printf(" LIGHT_GREEN:OFF--\n"); //串口输出灯的状态
gpio_set(LIGHT_BLUE,LIGHT_OFF); //蓝灯“暗”
printf(" LIGHT_BLUE:OFF--\n"); //串口输出灯的状态
}
} //for(;;)结尾
//(2)======主循环部分(结尾)========================================
} //main函数(结尾)
- 运行效果:
小灯会循环亮暗交替,每次小灯亮的颜色都会变换,颜色变换顺序是红、绿、黄、蓝、紫、青、白。



【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了