简介:面向STM32F103开发者,这份压缩包提供ADS1115高精度16位ADC的完整驱动与例程,解决I2C初始化、增益配置、多通道采样与数据换算等常见问题。资源共150个文件、2.82MB,主要包含37个h头文件与35个c源码文件,覆盖标准外设库驱动;另有hex烧录文件、uvprojx工程文件及uvoptx选项配置,可直接用Keil打开编译;o、crf等为构建中间产物,htm与txt为说明文档,bat脚本可用于工程清理。例程涉及I2C时序、ADS1115寄存器配置、PGA可编程增益、16位结果读取及实际电压换算,并支持LCD显示,适合做传感器数据采集、工业监控等精确测量项目。已有712人学习下载,代码结构清晰,移植到其他STM32系列也较为方便。
1. F1-ADS1115例程_STM32F103_ads1115_:给F103外扩16位ADC,到底要解决什么
“F1-ADS1115例程_STM32F103_ads1115_”这个标题,翻译过来就是:在STM32F103上跑通TI的ADS1115驱动,让这颗16位ADC通过I2C把模拟量读回来。F103内置ADC只有12位,分辨率在3.3V参考下约0.8mV,量电桥、PT100、电池电压这类小信号时噪声和分辨率都不够;ADS1115是Delta-Sigma架构的16位ADC,4路输入、内置可编程增益放大器PGA,I2C接口只占PB6/PB7两根线。这个例程解决的不只是“读出数据”,而是完整走通I2C时序、16位配置字、量程换算和排错流程。适合做传感器采集、变送器、电池监测的嵌入式工程师照着复现。不少人以为I2C读写通了例程就算跑通,真正翻车的地方其实是配置字写错、参考电压没搞清、量程系数乘错,这三点我会在第3、4章用可复现的方式拆开讲。
2. 硬件连接:最小系统、5V转3.3V电路和上拉电阻,I2C能不能通全看这里
ADS1115的例程代码只占一半功夫,另一半在硬件连接。F103最小系统、ADS1115供电、I2C上拉电阻、地址引脚配置,这些没弄对,代码再对也读不出数据。这一章先把硬件链路的每个节点过一遍,顺便把例程里默认的引脚和跳线对应上。
2.1 STM32F103最小系统与ADS1115供电:VDD就是参考电压
跑例程之前,先确认手头的stm32f103最小系统板是能独立工作的:8MHz晶振或内部RC、复位电路、BOOT0拉低、3.3V供电,SWD口接ST-Link,这套环境是例程能下载调试的前提。ADS1115的供电范围是2.0V到5.5V,但例程里几乎都用3.3V。原因有两个:一是F103的GPIO电平就是3.3V,I2C上下拉电平一致,不需要额外转换;二是ADS1115没有内部基准电压,它的参考电压就是VDD,用5V供电会让LSB步进变大,小信号分辨率反而变差。如果你拿到的是带板载LDO的ADS1115模块,上面通常有一颗3.3V稳压芯片,这时可以给模块供5V,但要注意模块的SDA/SCL上拉电阻是接到模块自己的VDD还是接到了5V,这直接决定F103引脚会不会被灌到5V。我一般建议:F103和ADS1115都统一3.3V供电,省掉电平转换的麻烦。
2.2 ADDR引脚决定I2C地址:0x48、0x49、0x4A、0x4B怎么选
ADS1115的7位I2C地址由ADDR引脚的电平决定,例程里默认地址是0x48,对应ADDR接地。ADDR可以接GND、VDD、SDA或SCL,四种接法对应四个不同地址,具体如下:
| ADDR引脚接法 | 7位地址 | 写地址(左移1位) | 读地址(左移1位或|1) | |---|---|---|---| | GND | 0x48 | 0x90 | 0x91 | | VDD | 0x49 | 0x92 | 0x93 | | SDA | 0x4A | 0x94 | 0x95 | | SCL | 0x4B | 0x96 | 0x97 |
注意0x90并不等于0x48直接左移一位后那个“写字节”,如果你在例程里看到地址宏定义的是0x90而不是0x48,说明它已经把读写位拼进去了。很多移植翻车就翻在这里:例程里写的是0x90,你按0x48去对照数据手册,怎么都对不上。多片ADS1115挂同一条I2C总线时,把每片的ADDR分别接GND、VDD、SDA、SCL,就能得到四个独立地址,例程里用ADS1115_ADDR这个宏区分,改宏比改代码快。
2.3 电平匹配与上拉电阻:5V供电时怎么处理SDA/SCL
I2C是开漏架构,SCL和SDA必须接上拉电阻才能输出高电平。F103的PB6/PB7虽然标注了FT(5V容忍),但I2C外设的电气特性建议不要超过VDD+0.3V,所以最稳妥的做法是:上拉电阻接到3.3V,而不是接到ADS1115模块的VDD。如果模块是5V供电的,又不想额外飞线,可以用独立的上拉电阻从F103的3.3V接到SDA/SCL,前提是模块的SDA/SCL引脚不要自己再对VDD上拉,否则两个上拉并联还会产生额外灌电流。上拉电阻阻值方面,100kHz标准模式用4.7k,400kHz快速模式用2.2k到3.3k,例程默认走100kHz,4.7k就行。至于网上常说的 stm32f103 5v转3.3v电路,如果模块本身没有LDO,需要自己降压供电,用一个AMS1117-3.3就够了,别用电阻分压给ADC供电——负载变化时分压点电压会飘,直接体现在读数上。
3. 先读懂ADS1115的Config寄存器:增益PGA、数据速率DR和单次/连续转换怎么设
ADS1115例程的核心不在读取函数,而在那个16位的Config寄存器。很多人在例程里改一个量程、改一个采样率,改完发现读数不对,就是因为只改了配置字里的一两个位,其他位被覆盖了。这一章把Config寄存器拆开讲,并把例程默认配置0x8583逐位还原。
3.1 Config寄存器逐位拆解:OS、MUX、PGA、MODE、DR位怎么拼
ADS1115的Config寄存器地址是0x01,16位,每一位含义如下:
| 位序号 | 名称 | 含义 |
|---|---|---|
| 15 | OS | 单次转换启动位:写1启动转换;读回时0表示转换完成 |
| 14:12 | MUX | 输入多路选择:000=AIN0-AIN1差分,100=AIN0-GND单端,101=AIN1-GND,110=AIN2-GND,111=AIN3-GND |
| 11:9 | PGA | 增益:000=±6.144V,001=±4.096V,010=±2.048V,011=±1.024V,100=±0.512V,101=±0.256V |
| 8 | MODE | 1=单次转换,0=连续转换 |
| 7:5 | DR | 数据速率:000=8SPS,001=16SPS,010=32SPS,011=64SPS,100=128SPS,101=250SPS,110=475SPS,111=860SPS |
| 4 | COMP_MODE | 比较器模式,例程一般不用,置0 |
| 3 | COMP_POL | 比较器极性,置0 |
| 2 | COMP_LAT | 比较器锁存,置0 |
| 1:0 | COMP_QUE | 比较器队列:00=触发即响应,11=禁用比较器,例程默认11 |
例程默认配置0x8583,二进制是1000 0101 1000 0011。逐位拆开:OS=1启动单次转换,MUX=000即AIN0-AIN1差分输入,PGA=101即±0.256V,MODE=1单次转换,DR=100即128SPS,COMP_QUE=11禁用比较器。这个默认配置意味着:例程把输入范围压缩到±0.256V,如果你想量0到3.3V的电压,不改PGA直接读,读满量程也只有0.256V,出来的电压值完全不对。这就是大多数“例程读数不对”的根源。
3.2 配置字怎么拼:用C宏把位数拼出来,别手算16进制
手算16位二进制很容易错,我一般用移位宏把配置字拼出来,代码可读性和可维护性都好很多,后面想改哪个参数就改哪一行:
#define ADS1115_OS_START (1 << 15) // 启动单次转换 #define ADS1115_MUX_AIN0_AIN1 (0 << 12) // 差分:AIN0-AIN1,例程默认 #define ADS1115_MUX_AIN0_GND (4 << 12) // 单端:AIN0对GND #define ADS1115_MUX_AIN1_GND (5 << 12) // 单端:AIN1对GND #define ADS1115_MUX_AIN2_GND (6 << 12) #define ADS1115_MUX_AIN3_GND (7 << 12) #define ADS1115_PGA_6144 (0 << 9) // ±6.144V #define ADS1115_PGA_4096 (1 << 9) // ±4.096V #define ADS1115_PGA_2048 (2 << 9) // ±2.048V #define ADS1115_PGA_1024 (3 << 9) #define ADS1115_PGA_512 (4 << 9) #define ADS1115_PGA_256 (5 << 9) // ±0.256V,例程默认 #define ADS1115_MODE_SINGLE (1 << 8) // 单次转换 #define ADS1115_DR_128 (4 << 5) // 128SPS #define ADS1115_DR_860 (7 << 5) // 860SPS,最高速 #define ADS1115_COMP_DISABLE (3 << 0) // 禁用比较器 #define ADS1115_CONFIG_DEFAULT \ (ADS1115_OS_START | ADS1115_MUX_AIN0_AIN1 | ADS1115_PGA_256 | \ ADS1115_MODE_SINGLE | ADS1115_DR_128 | ADS1115_COMP_DISABLE)这套宏展开后得到0x8583,和例程默认值一致。如果你想量单端3.3V,把宏换成ADS1115_MUX_AIN0_GND | ADS1115_PGA_6144,得到的配置字是0xC183(OS启动、MUX=AIN0-GND、PGA=±6.144V、单次、128SPS、禁用比较器)。逻辑说明:这三个宏分别负责输入通道、量程和转换模式,互不干扰;参数说明里最需要注意的是MUX值,单端模式下MUX的高三位是100到111,不是0到3,这个对应关系和STM32的ADC通道号完全是两套,别拿F103的ADC规则通道来套。
3.3 单次转换与连续转换:什么时候用MODE=1,什么时候用MODE=0
例程默认MODE=1用单次转换,原因是省电且行为可预测:每次写入Config寄存器触发一次转换,转换完成后OS位自动清零,结果锁存在Conversion寄存器里。我们只需要估算转换时间(约1/DR秒),延时后再读,就能拿到这次触发对应的确定结果。连续模式MODE=0适合需要持续监测的场景,上电后自动循环转换,读Conversion随时能拿到最新值,但问题是你不知道读数对应的是哪个时刻的模拟输入,在输入快速变化时会有“新旧数据混读”的错觉。我一般把单次模式用在例程调试和电池监测这类低频场景,把连续模式用在需要配合ALERT/RDY引脚做中断唤醒的场景。另外注意:单次模式下OS位写1启动转换,如果你把这位置成0再写进Config,转换根本不会触发,读回来的数据是不会更新的旧值。
4. 在STM32F103上把例程跑通:软件I2C的完整读取流程和最小代码
例程通常提供标准库版本的I2C驱动,这里有一个选型问题:用F103的硬件I2C外设,还是用GPIO模拟的软件I2C?例程文件里如果不是CubeMX生成的工程,很多是软件I2C实现的,我强烈建议你优先跑软件I2C版本。原因后面第5章会细说:F103的硬件I2C在事件标志处理上容易卡死,调试起来像个黑匣子;软件I2C每一拍时序都看得见,逻辑分析仪一抓就知道问题在哪。这一章给出最小可用的软件I2C代码。
4.1 用CubeMX初始化两个开漏GPIO,然后在例程文件夹里找到引脚定义
F103的I2C引脚例程默认是PB6(SCL)和PB7(SDA),如果你用CubeMX重新生成工程,只需要把这两个引脚配置成开漏输出、速度50MHz、上拉使能。CubeMX工程生成之后,例程文件夹里main.c的MX_GPIO_Init函数就是引脚初始化入口,不要自己再去翻固件库的gpio.c。软件I2C不需要配置I2C外设,所以CubeMX里直接跳过I2C选项,只用GPIO。
__HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出 GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 50MHz HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);逻辑说明:开漏输出模式下,GPIO输出低电平时拉低总线,输出高电平时释放总线,由外部上拉电阻把电平拉高,这正好符合I2C协议对开漏的要求。参数说明:内部上拉和外部上拉可以共存,协同提供上拉电流;速度选HIGH是为了让沿更陡,但软件I2C实际跑100kHz左右,LOW也够用,关键是开漏这一条不能改。
4.2 软件I2C四段基础时序:起始、停止、发送字节和应答检测
软件I2C的核心是SCL高电平时SDA的跳变。起始条件是SCL高时SDA从高拉低,停止条件是SCL高时SDA从低拉高;发送字节则是SCL低时改变SDA电平,SCL高时被采样。这四段代码是例程里最底层的东西,写完基本不用再动:
static void I2C_Delay(void) { for (volatile uint32_t i = 0; i < 200; i++); // 72MHz下约2us } static void I2C_Start(void) { SDA_HIGH(); SCL_HIGH(); I2C_Delay(); SDA_LOW(); I2C_Delay(); // SCL高期间SDA下降沿 SCL_LOW(); I2C_Delay(); // 拉低SCL准备传数据 } static void I2C_Stop(void) { SDA_LOW(); SCL_HIGH(); I2C_Delay(); SDA_HIGH(); I2C_Delay(); // SCL高期间SDA上升沿 } static void I2C_SendByte(uint8_t byte) { for (uint8_t i = 0; i < 8; i++) { if (byte & 0x80) SDA_HIGH(); else SDA_LOW(); byte <<= 1; I2C_Delay(); SCL_HIGH(); I2C_Delay(); // 从设备在SCL高电平采样 SCL_LOW(); I2C_Delay(); } }逻辑说明:I2C_Delay决定了SCL时钟周期,每个bit高低电平各一次延时,8个bit加一个ACK位,一个字节大概要9个延时周期,算下来100kHZ左右,完全符合ADS1115的I2C时序要求。参数说明:如果你把主频改到不同的值,I2C_Delay里的200要按实际主频缩放,否则SCL频率会偏离预期,在快速模式设备上可能直接通信失败。SDA_HIGH和SDA_LOW就是HAL_GPIO_WritePin的封装,不再展开。
4.3 完整读取一次电压:写指针寄存器、写配置、等待转换、读两字节
ADS1115的读流程是固定的四步:先写指针寄存器指向Config,写入配置字触发转换;等待转换完成;再写指针寄存器指向Conversion;最后连续读两个字节合成16位结果。下面是完整函数:
uint8_t ADS1115_WriteConfig(uint16_t config) { I2C_Start(); if (!I2C_SendByte(ADS1115_ADDR << 1)) { I2C_Stop(); return 1; } // 写地址0x90 I2C_SendByte(0x01); // 指针指向Config寄存器 I2C_SendByte((config >> 8) & 0xFF); // 先写高字节 I2C_SendByte(config & 0xFF); // 再写低字节 I2C_Stop(); return 0; } int16_t ADS1115_ReadConversion(void) { int16_t raw = 0; I2C_Start(); if (!I2C_SendByte(ADS1115_ADDR << 1)) { I2C_Stop(); return 0; } I2C_SendByte(0x00); // 指针指向Conversion寄存器 I2C_Start(); // 重复起始,切换为读 I2C_SendByte((ADS1115_ADDR << 1) | 1); // 读地址0x91 uint8_t hi = I2C_ReadByte(1); // 主机回复ACK,继续读 uint8_t lo = I2C_ReadByte(0); // 主机回复NACK,结束 I2C_Stop(); raw = (int16_t)((hi << 8) | lo); return raw; } float ADS1115_GetVoltage(uint16_t config, float lsb_mv) { ADS1115_WriteConfig(config); HAL_Delay(10); // 128SPS对应7.8ms,留余量 int16_t raw = ADS1115_ReadConversion(); return raw * lsb_mv; // 单位mV }逻辑说明:写配置时主机发送设备地址0x90后,ADS1115会在第9个SCL时钟拉低SDA应答,I2C_SendByte的返回值就是ACK位;读转换结果时,必须在读到第二个字节时回NACK,否则ADS1115会继续输出下一个字节,总线上数据错位。参数说明:HAL_Delay(10)是按默认128SPS设置的,如果你把DR改成860SPS,延时可以缩到3ms,改到8SPS时则要等130ms以上,延时不够会读到上一次的旧数据,这是例程移植时最常见的时序坑。
4.4 电压换算表:PGA不同,LSB系数就不同,别乘错
ADS1115是16位ADC,带符号输出,每个数字量对应的电压由PGA决定。例程默认PGA=±0.256V,但实际项目里经常改成±4.096V或±6.144V,换算系数如下:
| PGA配置 | 满量程范围 | 电压分辨率LSB |
|---|---|---|
| 000 | ±6.144V | 0.1875mV |
| 001 | ±4.096V | 0.125mV |
| 010 | ±2.048V | 0.0625mV |
| 011 | ±1.024V | 0.03125mV |
| 100 | ±0.512V | 0.015625mV |
| 101 | ±0.256V | 0.0078125mV |
注意LSB的单位是mV,不是V。例程里如果你直接拿raw * 0.1875去算6.144V档,出来的单位是mV,再除以1000才是V。另外int16_t是有符号数,差分输入时负电压读出来是负数,直接用这个数乘LSB就得到带符号电压;如果误用了uint16_t,负电压会变成65535附近的巨大正数,这也是例程里读数突变的一个隐蔽原因。
5. 例程移植中的5个坑:从I2C死锁到Pack路径,每条都是经验换来的
跑通例程之后,移植到自己的板子或改成自己的应用场景时,问题才会真正暴露。这一章把我在F103和ADS1115组合上实际踩过的坑按“现象、原因、解决”写清楚,帮你省掉逐个排查的时间。每条都可以直接对号入座。
5.1 读数永远是0xFFFF或0x0000:先查地址再查上拉
现象:例程编译下载后,串口打印的原始值是65535或者0,怎么触发都变不了。
原因:最常见的是I2C地址不对。例程宏ADS1115_ADDR通常定义成0x48,但代码里写的是ADS1115_ADDR << 1,也就是0x90;有些移植者直接把0x48当写地址用,导致总线上没有设备应答,读回的数据总线被上拉电阻拉到高电平,得到0xFFFF。另一个常见原因是模块上的ADDR引脚被接了VDD,实际地址变成了0x49,例程里却还是0x48。
解决:拿起原理图或万用表确认ADDR引脚电平,然后改宏。改完之后用逻辑分析仪抓ACK位,在第九个时钟看SDA有没有被拉低,这是判断I2C设备是否存在的最快方法。还有一种隐蔽情况:只接了SCL和SDA,忘了接VDD或GND,ADS1115根本不上电,这时候抓总线会发现SDA一直是高,没有任何ACK。
5.2 硬件I2C卡死在EV5/EV6:事件标志死等是F103的老问题
现象:用CubeMX生成硬件I2C工程,程序运行到while (__HAL_I2C_GET_FLAG(I2C1, I2C_FLAG_SB) == RESET)之类的地方再也不往下走,看仿真器,PC指针停在等待循环里。
原因:F103的硬件I2C外设对总线状态很敏感,总线上的毛刺或者上拉太弱会导致起始条件没被正确识别,标志位永远不置位。标准库和HAL库生成的代码,绝大部分都是死等标志,没有超时机制,一旦卡住,没有后悔药。
解决:有三种做法。第一种是给所有等待加超时计数,超时后复位I2C外设并重试;第二种是干脆不用硬件I2C,直接用上一章的软件I2C,这是最可靠的做法;第三种是降低I2C时钟到100kHz以下。我自己的习惯是从一开始就用软件I2C,ADS1115采样率最高860SPS,数据量不大,软件I2C完全扛得住,省下的调试时间远超那点CPU开销。
5.3 电压读数比万用表偏低:PGA选太大,参考电压其实是VDD
现象:用例程默认配置量3.3V,读出来只有0.25V左右,量0V倒是准的。
原因:这就是第3章说的默认PGA=±0.256V,输入超过满量程,ADC输出饱和在32767附近,按LSB换算后自然不对。还有一种情况是PGA选对了但读数仍偏低,那是信号源内阻太大——ADS1115内部采样电容充电需要时间,高阻信号源会拉低采样电压。
解决:先根据你要量的最大电压选PGA,量0-3.3V就选±6.144V或±4.096V,然后把配置字里的PGA宏对应改掉。高阻信号源前面加一级运放跟随器。最后记住一个关键点:ADS1115没有独立基准引脚,它的参考电压就是VDD,VDD如果是LDO输出的3.28V,实际LSB就是3.28/32768,不是理论值。要求高时用万用表实测VDD,把换算公式里的lsb_mv改成VDD/32768*1000。
5.4 连续模式读数跳变、毛刺多:DR太高,数字滤波来不及
现象:连续模式下打印的电压值偶尔跳几十个LSB,甚至瞬间跳变几百LSB,看起来像接触不良。
原因:ADS1115的DR设为860SPS或475SPS时,内部数字滤波器带宽宽,噪声明显增加,这是Delta-Sigma ADC的正常特性。如果你在读取的同时发生了一次转换更新,还可能读到新旧数据交界的瞬时值。电源纹波大时,参考电压VDD上的扰动会直接耦合进测量结果。
解决:把DR降到128SPS或64SPS,这个档位下数字滤波效果明显变好。程序上做两次读取:连续读两个结果,如果差值超过几个LSB就再读一次,取最后一次;或者做简单的滑动平均。电源方面,ADS1115的VDD引脚靠近放一个1uF陶瓷电容加一个0.1uF电容,如果和你板子上的电源是一根走线,单独飞线供电也比共用好。
5.5 Keil工程打开报错或找不到芯片:Pack路径和例程文件夹别搞混
现象:下载的“F1-ADS1115例程_STM32F103_ads1115_”用Keil5打开后,Device栏是空的,或者编译报错找不到芯片头文件,下载时提示No Target。
原因:多半是没装STM32F1系列DFP pack包,或者例程对应的芯片型号和你的板子不一致。例程文件夹里可能包含多个工程文件,有的是针对F103C8,有的是针对F103ZET6,打开错了工程自然报错。中文路径或路径里带空格也会让Keil的芯片数据库索引出问题。
解决:先去Keil官网或Pack Installer里装最新的Keil.STM32F1xx_DFP包,然后打开例程目录下带MDK-ARM字样的uvprojx工程,在Options for Target里把Device改成你板子实际用的型号。如果例程文件夹里同时有Src目录和MDK-ARM目录,用后者,不要自己新建工程去把Src里的文件一个个加进来,那会漏掉启动文件和系统时钟配置。那篇“stm32f103 keil mdk工程搭建与st-link调试全流程”讲的就是这套环境的完整搭法,照着走一遍能少踩不少坑。另外尽量把整个例程文件夹拷贝到纯英文路径再打开,keil对中文目录支持很差。
6. 把例程改成多通道轮询:MUX切换、单次触发和均值滤波
例程默认只读AIN0-AIN1一组差分输入,实际项目里往往要同时监测好几路电压,这时不需要改太多代码,只要动MUX位就行。我的做法是把配置字拆成“固定部分”和“通道部分”,用循环依次写入通道配置,单次转换并读取,最后对每个通道取三次平均值:
const uint16_t config_fixed = ADS1115_OS_START | ADS1115_PGA_6144 | ADS1115_MODE_SINGLE | ADS1115_DR_128 | ADS1115_COMP_DISABLE; const uint16_t mux_table[4] = { ADS1115_MUX_AIN0_GND, ADS1115_MUX_AIN1_GND, ADS1115_MUX_AIN2_GND, ADS1115_MUX_AIN3_GND }; for (uint8_t ch = 0; ch < 4; ch++) { uint32_t sum = 0; for (uint8_t i = 0; i < 3; i++) { ADS1115_WriteConfig(config_fixed | mux_table[ch]); HAL_Delay(10); sum += ADS1115_ReadConversion(); } int16_t raw_avg = (int16_t)(sum / 3); float volt_mv = raw_avg * 0.1875f; printf("CH%d: %ld mV\r\n", ch, (long)(volt_mv)); }注意每次切换通道都要重新写Config寄存器,因为MUX位是在写入时被采样的。三次采样后取平均能滤掉大部分随机噪声,但不要平均太多次,超过几十次会把真实变化也抹掉。验证方法很简单:拿一个精密可调电源或电位器分压,给每个通道输入已知电压,看读数误差是否在2个LSB以内。我最早做这个例程移植时图省事用了硬件I2C,结果卡死在事件标志上查了一下午,最后换成软件I2C才通,从那以后我拿到任何例程第一件事就是检查GPIO配置和地址宏,改完任何配置字都会先读回Config确认写进去的值。希望帮到你。
本文还有配套的精品资源,点击获取