ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32驱动DS3231高精度时钟:从I²C到闹钟与温度补偿实战

STM32驱动DS3231高精度时钟:从I²C到闹钟与温度补偿实战 1. 为什么DS3231是STM32时钟项目的分水岭搞STM32的人迟早会碰到一个尴尬事用内部RTC做出来的时钟跑一周能差出好几分钟。我最早做的一个实验室计时器用的就是STM32F103自带的RTC加32.768kHz晶振结果夏天和冬天走时速度完全不一样一个月下来累计误差能到十几分钟。后来换成DS3231同样的板子、同样的固件框架半年下来误差还在半分钟以内。这个差距不是靠软件校准能弥补的根源在于DS3231内部集成了温度补偿晶振TCXO而STM32内部RTC用的是普通外部晶振温漂完全看晶振的体质和负载电容的匹配程度。DS3231这颗芯片在DIY圈子里被叫做“时钟芯片里的劳斯莱斯”价格却不贵模块通常十几块钱。它通过I²C总线和STM32通信只需要两根信号线就能读写年月日时分秒还自带两路闹钟、一个可编程方波输出和内部温度传感器。对于做毕业设计、智能家居中控、数据记录仪、定时开关这类需要“时间可信”的项目来说DS3231几乎是绕不开的选择。这篇文章面向的是已经会点STM32基础、能点亮LED、能跑通串口打印的开发者。我会把DS3231的硬件连接、I²C驱动编写、时间读写、闹钟配置、温度读取、掉电保持、误差校准这些环节全部拆开讲代码基于STM32 HAL库用Keil5和CubeMX搭建工程。中间会穿插我实际踩过的坑比如I²C死锁、上拉电阻选值、备用电池选型、寄存器地址搞错导致读出来全是0xFF这些问题。看完你至少能少走两三个晚上的弯路。2. 硬件设计DS3231模块怎么接才稳2.1 模块选型与引脚定义市面上常见的DS3231模块有两种一种是小板子带CR2032电池座和AT24C32 EEPROM另一种是裸芯片加必要外围。我建议新手直接买带电池座的模块省去自己画电池供电电路的麻烦。模块上通常引出6个引脚VCC、GND、SDA、SCL、SQW、32K。其中SQW是可编程方波输出32K是32.768kHz固定输出做闹钟中断用SQW就够了。接线逻辑很简单VCC接STM32的3.3VGND共地SDA和SCL分别接STM32的I²C引脚。以STM32F103C8T6为例I²C1的默认引脚是PB6SCL和PB7SDA重映射后是PB8和PB9。我一般用PB6/PB7因为CubeMX里配置起来最顺手。注意DS3231模块上的VCC是给I²C接口和芯片逻辑供电的备用电池只给内部振荡器和时间寄存器供电。也就是说主电源断了之后I²C通信会停但时间继续走。重新上电后读出来的时间仍然是准的。2.2 上拉电阻到底用多大I²C总线是开漏输出SDA和SCL必须接上拉电阻才能拉高。很多DS3231模块已经自带了4.7kΩ或10kΩ的上拉电阻你直接接线就能用。但如果你是自己画板子或者模块上没带上拉就必须在STM32端补上。上拉电阻的取值有个经验公式R_min (VDD - VOL) / IOLR_max tr / (0.8473 × Cb)。说人话就是电阻太小低电平时灌电流太大可能烧端口电阻太大上升沿变缓高速通信时波形还没拉高就被拉低了导致通信失败。标准模式100kHz下4.7kΩ是万金油快速模式400kHz下2.2kΩ到4.7kΩ都行。我实测下来3.3V系统、总线电容不超过200pF的情况下4.7kΩ最稳。有个坑要特别提醒如果你用的模块自带10kΩ上拉而你的STM32开发板上I²C引脚又焊了4.7kΩ上拉两个电阻并联后等效阻值变成约3.2kΩ一般不会出问题。但如果两边都是2.2kΩ并联后只有1.1kΩ低电平灌电流可能超过STM32端口的20mA极限。所以接线前最好用万用表量一下SDA对VCC的阻值确认上拉情况。2.3 备用电池的选择与寿命估算DS3231的备用电池一般用CR2032纽扣电池标称电压3V容量约220mAh。DS3231在电池供电模式下的典型电流是3μA左右温度补偿开启时会间歇性跳到几百微安但平均下来很小。按3μA算220mAh / 0.003mA ≈ 73333小时 ≈ 8.3年。实际因为温度补偿电路会周期性工作寿命大概在5到8年之间足够覆盖大多数项目的生命周期。但这里有个细节电池座的质量很关键。我买过一批便宜模块电池座弹片接触不良稍微震动一下就断电时间直接归零。后来换用带焊接引脚的电池座问题才解决。另外如果你把模块放在高温环境里比如靠近电机驱动或电源模块电池自放电会加快寿命可能打对折。提示第一次上电后先写一次时间然后断开主电源等几分钟再上电读出来的时间如果继续走了说明电池供电正常。如果时间归零检查电池电压是否低于2.5V或者电池座是否接触良好。3. I²C驱动层从CubeMX配置到寄存器读写3.1 CubeMX里的I²C参数怎么填打开CubeMX在Connectivity里选I²C1模式选I2C速度模式选Standard Mode还是Fast Mode取决于你的需求。DS3231支持400kHz快速模式但如果你总线上还挂了其他慢速器件比如某些温湿度传感器建议统一用100kHz避免兼容性问题。时钟配置里有个地方容易忽略I²C的时钟源来自APB1F103系列APB1最高36MHz。CubeMX会自动计算分频值你只需要确认最终SCL频率是100kHz或400kHz就行。另外Duty Cycle在快速模式下选2:1还是16:9一般选2:1兼容性更好。生成代码后HAL库会初始化I²C外设但不会自动使能上拉。如果你用的是STM32内部上拉约40kΩ阻值太大100kHz下波形上升沿可能超过1μs导致通信不稳定。所以还是老老实实用外部4.7kΩ上拉。3.2 DS3231寄存器地址速查DS3231的寄存器地址从0x00到0x12共19个寄存器。常用的几个寄存器地址名称功能数据格式0x00Seconds秒BCDbit7为CH0x01Minutes分BCD0x02Hours时BCDbit6为12/24模式0x03Day星期BCD1-70x04Date日BCD0x05Month月BCDbit7为世纪位0x06Year年BCD00-990x07Alarm1 Seconds闹钟1秒BCD0x08Alarm1 Minutes闹钟1分BCD0x09Alarm1 Hours闹钟1时BCD0x0AAlarm1 Day/Date闹钟1日/星期BCD0x0BAlarm2 Minutes闹钟2分BCD0x0CAlarm2 Hours闹钟2时BCD0x0DAlarm2 Day/Date闹钟2日/星期BCD0x0EControl控制寄存器bit7 EOSCbit6 BBSQWbit5 CONVbit2 INTCNbit1 A2IEbit0 A1IE0x0FStatus状态寄存器bit7 OSFbit3 EN32kHzbit2 BSYbit1 A2Fbit0 A1F0x10Aging Offset老化偏移有符号数0x11MSB of Temp温度高8位有符号数0x12LSB of Temp温度低2位每单位0.25°CBCD码的意思是比如秒寄存器读到0x59表示59秒读到0x23表示23秒。高4位是十位低4位是个位。写时间的时候也要按BCD格式写不能直接写十进制数。3.3 底层读写函数实现HAL库提供了HAL_I2C_Mem_Read和HAL_I2C_Mem_Write可以直接读写指定寄存器地址。DS3231的7位I²C地址是0x68左移一位后写地址是0xD0读地址是0xD1。HAL库函数里填0x68 1也就是0xD0。#define DS3231_ADDR (0x68 1) uint8_t DS3231_ReadReg(uint8_t reg) { uint8_t data 0; HAL_I2C_Mem_Read(hi2c1, DS3231_ADDR, reg, I2C_MEMADD_SIZE_8BIT, data, 1, 100); return data; } void DS3231_WriteReg(uint8_t reg, uint8_t data) { HAL_I2C_Mem_Write(hi2c1, DS3231_ADDR, reg, I2C_MEMADD_SIZE_8BIT, data, 1, 100); }这两个函数是所有上层功能的基础。注意超时时间我设了100ms实际100kHz下读写一个字节也就几十微秒100ms足够容错。如果你发现经常超时先检查硬件连接和上拉电阻再考虑是不是I²C总线被其他器件拉死了。BCD和十进制转换的辅助函数uint8_t BCD2DEC(uint8_t bcd) { return (bcd 4) * 10 (bcd 0x0F); } uint8_t DEC2BCD(uint8_t dec) { return ((dec / 10) 4) | (dec % 10); }这两个函数看起来简单但写时间的时候如果忘了转换比如想把23分写成0x23结果直接写了十进制23也就是0x17读出来就变成17分了。我第一次调的时候就被这个坑了一个小时。4. 时间读写与掉电保持实战4.1 上电初始化流程DS3231上电后状态寄存器的OSF位Oscillator Stop Flag会置1表示振荡器曾经停过时间可能不准。所以初始化时要先读状态寄存器如果OSF为1就重新写一次时间然后清除OSF位。void DS3231_Init(void) { uint8_t status DS3231_ReadReg(0x0F); if (status 0x80) { // 振荡器停过时间不可信重新设置 DS3231_SetTime(2025, 1, 15, 3, 10, 30, 0); // 清除OSF位 DS3231_WriteReg(0x0F, status 0x7F); } // 确保EOSC位为0电池供电时振荡器继续运行 uint8_t ctrl DS3231_ReadReg(0x0E); ctrl ~0x80; DS3231_WriteReg(0x0E, ctrl); }EOSC位在控制寄存器bit7默认是0表示电池供电时振荡器正常运行。如果你不小心把它置1了电池供电时振荡器会停时间就不走了。有些模块出厂时这个位是0但如果你读出来是1记得清掉。4.2 写时间与读时间的完整代码写时间函数需要把年月日时分秒星期都写进去。注意DS3231的星期寄存器是1到7对应你自己定义的星期几它本身不关心具体是星期几只负责递增。void DS3231_SetTime(uint16_t year, uint8_t month, uint8_t date, uint8_t week, uint8_t hour, uint8_t min, uint8_t sec) { uint8_t buf[7]; buf[0] DEC2BCD(sec); buf[1] DEC2BCD(min); buf[2] DEC2BCD(hour); buf[3] DEC2BCD(week); buf[4] DEC2BCD(date); buf[5] DEC2BCD(month); buf[6] DEC2BCD(year % 100); HAL_I2C_Mem_Write(hi2c1, DS3231_ADDR, 0x00, I2C_MEMADD_SIZE_8BIT, buf, 7, 100); }读时间函数反过来把7个寄存器一次性读出来再转成十进制void DS3231_GetTime(uint16_t *year, uint8_t *month, uint8_t *date, uint8_t *week, uint8_t *hour, uint8_t *min, uint8_t *sec) { uint8_t buf[7]; HAL_I2C_Mem_Read(hi2c1, DS3231_ADDR, 0x00, I2C_MEMADD_SIZE_8BIT, buf, 7, 100); *sec BCD2DEC(buf[0] 0x7F); *min BCD2DEC(buf[1] 0x7F); *hour BCD2DEC(buf[2] 0x3F); *week BCD2DEC(buf[3] 0x07); *date BCD2DEC(buf[4] 0x3F); *month BCD2DEC(buf[5] 0x1F); *year 2000 BCD2DEC(buf[6]); }这里读的时候做了掩码处理因为秒寄存器的bit7是CH位时钟停止位小时寄存器的bit6是12/24模式位月寄存器的bit7是世纪位。掩掉这些位再转BCD避免读出奇怪的值。注意如果你用12小时模式小时寄存器的bit6为1bit5为AM/PM标志。我建议统一用24小时模式写时间时确保bit6为0。读出来如果发现小时数不对先检查这个位。4.3 掉电保持验证方法验证掉电保持很简单写一个时间比如2025年1月15日10点30分0秒然后拔掉VCC等5分钟再插上VCC读时间。如果读出来是10点35分左右说明电池供电正常。如果读出来还是10点30分0秒说明电池没起作用时间停了。我遇到过一种情况电池有电但模块上的二极管压降太大导致电池实际加到DS3231的电压只有2.4V低于2.3V的最低工作电压振荡器停振。解决办法是换用低压降的肖特基二极管或者直接把电池正极接到VBAT引脚跳过二极管。不过跳过二极管的话主电源供电时可能会给电池充电CR2032是不可充电的有安全隐患。所以还是建议保留二极管选压降小的型号。5. 闹钟与方波输出让时钟会“提醒”5.1 闹钟1的配置与中断触发DS3231有两路闹钟闹钟1可以精确到秒闹钟2只能到分钟。做定时提醒、整点报时、周期性唤醒MCU用闹钟1就够了。闹钟1的寄存器有4个秒、分、时、日/星期。每个寄存器的bit7是MASK位用来屏蔽该字段的匹配。比如你只想每天10点30分0秒触发就把秒、分、时的MASK位清零日/星期的MASK位置1。这样只要时分秒匹配不管日期都会触发。void DS3231_SetAlarm1(uint8_t hour, uint8_t min, uint8_t sec) { uint8_t buf[4]; buf[0] DEC2BCD(sec); // 秒MASK0 buf[1] DEC2BCD(min); // 分MASK0 buf[2] DEC2BCD(hour); // 时MASK0 buf[3] 0x80; // 日/星期MASK1忽略日期 HAL_I2C_Mem_Write(hi2c1, DS3231_ADDR, 0x07, I2C_MEMADD_SIZE_8BIT, buf, 4, 100); // 使能闹钟1中断INTCN1A1IE1 uint8_t ctrl DS3231_ReadReg(0x0E); ctrl | 0x05; // bit2 INTCN1, bit0 A1IE1 DS3231_WriteReg(0x0E, ctrl); // 清除闹钟1标志 uint8_t status DS3231_ReadReg(0x0F); status ~0x01; DS3231_WriteReg(0x0F, status); }配置完成后SQW引脚会在闹钟匹配时输出低电平如果INTCN1且A1IE1。你可以把SQW接到STM32的外部中断引脚比如PA0配置下降沿触发。中断服务函数里清除DS3231的A1F标志否则下次不会再次触发。5.2 方波输出的频率选择如果你不需要闹钟只想让DS3231输出一个固定频率的方波可以把INTCN位设为0然后通过控制寄存器的RS2和RS1位选择频率RS2RS1频率001Hz011.024kHz104.096kHz118.192kHz1Hz方波可以用来驱动秒闪LED或者给MCU提供一个精确的秒中断。我做过一个项目用1Hz方波触发STM32的外部中断在中断里更新OLED显示比用定时器轮询更省电因为MCU大部分时间可以休眠。void DS3231_SetSquareWave(uint8_t freq) { uint8_t ctrl DS3231_ReadReg(0x0E); ctrl ~0x18; // 清除RS2和RS1 ctrl ~0x04; // INTCN0方波输出 ctrl | (freq 3); // freq: 01Hz, 11.024kHz, 24.096kHz, 38.192kHz DS3231_WriteReg(0x0E, ctrl); }提示方波输出时SQW引脚是推挽输出不需要上拉电阻。但如果你之前接了上拉也没关系推挽输出会直接拉低拉高上拉电阻只是多消耗一点电流。5.3 闹钟中断的软件消抖与标志清除DS3231的闹钟标志A1F在状态寄存器bit0触发后置1。如果你不清除SQW会一直保持低电平STM32的外部中断会反复触发。所以中断服务函数里第一件事就是读状态寄存器确认A1F为1然后写0清除。void EXTI0_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); } void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin GPIO_PIN_0) { uint8_t status DS3231_ReadReg(0x0F); if (status 0x01) { status ~0x01; DS3231_WriteReg(0x0F, status); // 执行闹钟任务 alarm_flag 1; } } }这里有个细节I²C读写不能在中断里做太长时间100kHz下读写一个字节大概几十微秒可以接受。但如果你总线上还有其他慢速器件或者中断优先级很高建议在中断里只置标志在主循环里处理I²C清除操作。不过DS3231的SQW在A1F清除后会立即拉高所以清除操作要尽快否则中断会一直挂着。6. 温度读取与走时误差校准6.1 内部温度传感器的读取方法DS3231内部有个温度传感器精度±3°C虽然不如专用温度芯片但用来做粗略的温度补偿参考足够了。温度值存在0x11和0x12两个寄存器里0x11是高8位有符号数0x12的低2位是小数部分每单位0.25°C。float DS3231_GetTemperature(void) { uint8_t msb DS3231_ReadReg(0x11); uint8_t lsb DS3231_ReadReg(0x12); int8_t temp_msb (int8_t)msb; float temp temp_msb ((lsb 6) * 0.25f); return temp; }注意lsb只有bit7和bit6有效右移6位后得到0到3的值乘以0.25就是小数部分。比如msb0x1925lsb0x40bit61温度就是25.25°C。这个温度值每64秒更新一次如果你连续读可能读到旧值。不过做环境监测足够了。我试过用DS3231的温度和SHT30对比常温下差1°C左右高温下差2到3°C毕竟DS3231的传感器主要是给内部TCXO做补偿用的不是精密测温。6.2 老化偏移寄存器的校准原理DS3231的走时误差主要来自晶振的老化。芯片里有个老化偏移寄存器0x10可以微调晶振的负载电容从而调整走时速度。这个寄存器是有符号数范围-128到127每个单位大约影响0.1ppm。1ppm相当于每月误差约2.6秒所以1大约让时钟每月快0.26秒-1让时钟每月慢0.26秒。校准方法先让时钟跑一个月记录误差。如果快了10秒就需要减慢写入负值。10秒/月 ≈ 3.85ppm需要-38左右。但实际调整时不要一次写太多先写-10跑一周看效果再逐步逼近。void DS3231_SetAgingOffset(int8_t offset) { DS3231_WriteReg(0x10, (uint8_t)offset); }注意老化偏移寄存器在电池供电时也会保持所以校准一次就行。但温度变化大的环境TCXO会自动补偿老化偏移只是修正晶振本身的固有偏差不要指望它能解决所有误差。6.3 实测误差数据与校准记录我用一个DS3231模块做了半年测试室温20到28°C波动初始老化偏移为0。第一个月快了8秒写入-30后第二个月快了1秒第三个月慢了2秒。后来把偏移调到-25第四到第六个月累计误差在3秒以内。这个精度对于大多数项目已经足够了。如果你需要更高精度可以考虑用GPS的PPS信号定期校准DS3231或者用网络时间协议同步。但那是另一个话题了单靠DS3231本身做到每月几秒的误差已经是它的设计上限。7. 常见问题与避坑指南7.1 I²C通信失败排查表现象可能原因解决方法HAL_I2C_Mem_Read返回HAL_ERROR从机地址错误确认地址是0x6810xD0读出来全是0xFF上拉电阻缺失或太大补4.7kΩ上拉读出来全是0x00电源未接通或芯片损坏量VCC电压换模块偶尔通信失败总线电容过大或干扰降低SCL频率到100kHz缩短走线写时间后读出来不对BCD转换错误检查DEC2BCD和BCD2DEC时间不走EOSC位被置1或电池没电清EOSC位换电池闹钟不触发A1IE未使能或A1F未清除检查控制寄存器和状态寄存器温度读出来是负数有符号数处理错误用int8_t接收MSB7.2 I²C死锁的软硬件解法I²C死锁是STM32上最常见的问题之一。现象是SCL被从机拉低不放或者SDA被拉低主机无法发起新的传输。原因通常是通信过程中主机复位从机还在等时钟或者总线电容太大导致波形畸变。硬件解法在SCL和SDA上各串一个100Ω电阻限制灌电流。软件解法检测到总线死锁后把SCL配置为推挽输出手动发送9个时钟脉冲让从机释放SDA然后重新初始化I²C外设。void I2C_BusRecovery(void) { GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_6 | GPIO_PIN_7; gpio.Mode GPIO_MODE_OUTPUT_OD; gpio.Pull GPIO_PULLUP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, gpio); for (int i 0; i 9; i) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); } // 重新初始化I2C MX_I2C1_Init(); }这个函数在初始化时调用一次或者在检测到通信失败时调用能解决大部分死锁问题。7.3 备用电池的选型与更换时机CR2032是最常见的选择但要注意有些模块用的是CR1220容量只有40mAh寿命短很多。买模块时看清楚电池型号。另外电池座有正负极标识装反了不仅不供电还可能短路。更换电池的时机如果发现每次断电后时间都归零或者电池电压低于2.5V就该换了。测量电压时要在断电状态下量因为主电源供电时电池两端电压会被拉高量不准。提示换电池时最好在通电状态下换这样DS3231不会断电时间不会丢。但要注意别短路电池座的正负极。如果你不放心可以先写一个已知时间换完电池再读确认时间没归零。7.4 代码移植到不同STM32系列的注意事项F103和F407的I²C外设寄存器不同但HAL库接口是一样的。移植时主要改三个地方I²C句柄名称hi2c1还是hi2c2、GPIO引脚定义、时钟使能。另外F4系列的I²C有模拟滤波器和数字噪声滤波器配置时可以根据环境噪声决定是否开启。如果你从标准库移植到HAL库注意HAL_I2C_Mem_Read的超时参数单位是毫秒标准库的I2C_ReadBuffer没有超时机制容易卡死。所以HAL库在这方面更安全。8. 完整工程结构与代码组织建议8.1 文件划分与模块解耦我习惯把DS3231的驱动分成三个文件ds3231.h放宏定义和函数声明ds3231.c放底层读写和时间转换ds3231_app.c放应用层逻辑比如闹钟任务和温度显示。这样换项目时直接拷贝前两个文件就行应用层根据需求重写。头文件里定义好I²C地址、寄存器地址、函数原型用extern声明全局的I²C句柄。源文件里包含HAL库头文件实现具体逻辑。应用层只调用DS3231_GetTime和DS3231_SetAlarm1这些高层函数不直接碰寄存器。8.2 主循环的时间更新策略主循环里不要频繁读DS3231I²C通信虽然快但没必要每秒读几十次。我一般用1Hz方波中断或者定时器每秒读一次更新显示和逻辑判断。如果用了闹钟中断主循环只处理标志位时间读取可以降到每分钟一次减少总线占用。while (1) { if (sec_flag) { sec_flag 0; DS3231_GetTime(year, month, date, week, hour, min, sec); OLED_ShowTime(year, month, date, hour, min, sec); } if (alarm_flag) { alarm_flag 0; Buzzer_On(); HAL_Delay(500); Buzzer_Off(); } }8.3 调试打印与在线监控技巧调试I²C时串口打印是最直接的手段。把读到的原始寄存器和转换后的时间都打出来对比看哪一步出错。比如原始秒寄存器是0x59转换后应该是59如果打出来是89说明BCD转换写反了。另外可以用逻辑分析仪抓I²C波形看地址、寄存器、数据是否符合预期。没有逻辑分析仪的话用STM32的I²C错误中断也能定位问题比如AF应答失败说明从机没响应BERR总线错误说明时序异常。我在实际项目中还加了一个“时间戳”功能每次读写DS3231时把操作前后的系统滴答计数打出来算一下I²C传输耗时。100kHz下读写7个字节大概1.5ms如果超过5ms说明总线被拉低了需要检查硬件。9. 项目扩展从时钟到数据记录仪DS3231加上AT24C32 EEPROM很多模块自带就能做一个简单的数据记录仪。每分钟读一次温度把时间和温度存到EEPROM里通过串口导出。DS3231的闹钟可以唤醒STM32平时MCU休眠省电。如果要加显示OLED或LCD都可以。OLED用I²C接口的话注意和DS3231共用总线时地址不能冲突。DS3231是0x68OLED一般是0x3C或0x3D不冲突。但两个器件的上拉电阻要统一考虑别并联后阻值太小。再进一步可以加一个ESP8266或ESP32做网络对时每天用网络时间校准一次DS3231这样长期精度可以做到秒级。不过这就涉及网络编程了超出本文范围有兴趣的可以自己研究。最后分享一个我常用的调试技巧拿一个已知准确的时间源比如手机和DS3231同时开始计时每隔24小时对比一次连续记录一周。这样能快速判断DS3231的走时特性是快了还是慢了大概多少ppm然后决定老化偏移写多少。这个方法虽然土但比盲目写寄存器靠谱得多。
返回列表