描述
<h1>目录</h1>
<ul>
<li><a href="#1%E4%BB%8B%E7%BB%8D" target="_blank">1、介绍</a></li>
<li><a href="#2%E7%A1%AC%E4%BB%B6%E9%83%A8%E5%88%86" target="_blank">2、硬件部分</a>
<ul>
<li><a href="#21%E7%94%B5%E6%BA%90%E9%83%A8%E5%88%86" target="_blank">2.1电源部分</a></li>
<li><a href="#22-%E6%B8%A9%E6%B9%BF%E5%BA%A6%E6%98%BE%E7%A4%BA" target="_blank">2.2 温湿度显示</a></li>
<li><a href="#23-stm32%E5%A4%96%E5%9B%B4%E7%94%B5%E8%B7%AF" target="_blank">2.3 STM32外围电路</a></li>
<li><a href="#24-sht40%E6%A8%A1%E5%9D%97" target="_blank">2.4 SHT40模块</a></li>
</ul></li>
<li><a href="#3%E8%BD%AF%E4%BB%B6%E9%83%A8%E5%88%86" target="_blank">3、软件部分</a>
<ul>
<li><a href="#31-%E8%BD%AF%E4%BB%B6%E5%AE%9E%E7%8E%B0" target="_blank">3.1 软件实现</a></li>
<li><a href="#311%E6%95%B0%E7%A0%81%E7%AE%A1%E6%98%BE%E7%A4%BA%E6%95%B0%E5%AD%97" target="_blank">3.1.1数码管显示数字</a></li>
<li><a href="#312-adc%E9%87%87%E9%9B%86%E7%94%B5%E6%B1%A0%E7%94%B5%E5%8E%8B" target="_blank">3.1.2 ADC采集电池电压</a></li>
<li><a href="#313-%E6%A8%A1%E6%8B%9Fi2c%E8%BD%AF%E4%BB%B6i2c" target="_blank">3.1.3 模拟I2C(软件I2C)</a></li>
<li><a href="#314-%E4%B8%AD%E6%96%AD%E5%94%A4%E9%86%92" target="_blank">3.1.4 中断唤醒</a></li>
<li><a href="#32-%E8%BD%AF%E4%BB%B6i2c%E8%AF%A6%E8%A7%A3" target="_blank">3.2 软件I2C详解</a></li>
<li><a href="#321-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E7%94%A8%E8%BD%AF%E4%BB%B6i2c" target="_blank">3.2.1 为什么要用软件I2C?</a></li>
<li><a href="#322-i2c%E9%80%9A%E4%BF%A1%E5%8E%9F%E7%90%86%E8%A7%A3%E9%87%8A" target="_blank">3.2.2 I2C通信原理解释</a></li>
<li><a href="#323-i2c%E9%80%9A%E4%BF%A1%E8%BF%87%E7%A8%8B" target="_blank">3.2.3 I2C通信过程</a></li>
<li><a href="#324-i2c%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0%E6%96%B9%E6%B3%95" target="_blank">3.2.4 I2C代码实现方法</a></li>
<li><a href="#325-%E5%AE%9E%E7%8E%B0%E4%B8%8E%E6%B8%A9%E6%B9%BF%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%E7%9A%84i2c%E9%80%9A%E4%BF%A1" target="_blank">3.2.5 实现与温湿度传感器的I2C通信</a></li>
<li><a href="#33-%E4%BC%98%E5%8C%96%E7%A8%8B%E5%BA%8F%E9%99%8D%E4%BD%8E%E5%8A%9F%E8%80%97%E7%9A%84%E6%96%B9%E6%B3%95" target="_blank">3.3 优化程序降低功耗的方法</a></li>
<li><a href="#34-%E7%A8%8B%E5%BA%8F%E7%83%A7%E5%BD%95%E6%AD%A5%E9%AA%A4" target="_blank">3.4 程序烧录步骤</a></li>
</ul></li>
<li><a href="#4-%E6%95%85%E9%9A%9C%E6%8E%92%E9%99%A4" target="_blank">4 故障排除</a></li>
<li><a href="#5-%E9%A1%B9%E7%9B%AE%E5%B1%9E%E6%80%A7" target="_blank">5 项目属性</a></li>
<li><a href="#6-%E5%BC%80%E6%BA%90%E5%8D%8F%E8%AE%AE" target="_blank">6 开源协议</a></li>
<li><a href="#7-%E5%A4%A7%E8%B5%9Blogo%E9%AA%8C%E8%AF%81" target="_blank">7 大赛LOGO验证</a></li>
<li><a href="#8-%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91" target="_blank">8 演示视频</a></li>
<li><a href="#%E5%86%99%E5%9C%A8%E5%90%8E%E9%9D%A2" target="_blank">写在后面</a></li>
<li><a href="#%E5%8F%82%E8%80%83" target="_blank">参考</a></li>
</ul>
<h1>1、介绍</h1>
<p>在日常生活中,无论是家庭、办公室还是工作室,适宜的温湿度环境对于人体舒适度、工作效率乃至家居设备的保养都至关重要。然而,传统的温湿度计存在精度不足、需频繁更换一次性电池等问题,既不方便也不环保。</p>
<p>于是,我萌生了设计一款集低功耗、高精度、低成本于一身的桌面温湿度计的想法。这款产品的核心优势在于它巧妙地平衡了性能与成本,提供一个既实用又环保的温湿度解决方案。它具有以下特点:</p>
<p>(1)<span style="font-size:18px;color:#00CED1">低功耗:</span>考虑到长期使用的需求,本次设计选择低功耗的硬件组件和优化的软件算法。<span style="font-size:16px;color:#1E90FF">STM32G030K6T6</span>作为MCU,以其出色的能效比和丰富的外设资源,成为这款传感器的理想之选。通过精细的电源管理和休眠模式设计,能大幅度降低能耗,延长单次充电后的使用时间。</p>
<p>(2)<span style="font-size:18px;color:#FF7F50">高精度测量:</span>为了确保温湿度数据的准确性,本次设计选用了<span style="font-size:16px;color:#1E90FF">瑞士盛思锐SHT40</span>作为温湿度传感器。SHT40以其卓越的测量精度和稳定性著称,能够在广泛的温度和湿度范围内提供可靠的数据。</p>
<p>(3)<span style="font-size:18px;color:#9932CC">低成本:</span>通过精选高性价比的元器件和优化设计方案,温湿度传感器成本整体控制在<span style="font-size:18px;color:red"><strong>30元以内</strong> </span>(不含运费,立创商城价格)</p>
<p>(4)<span style="font-size:18px;color:#228B22">可充电:</span>采用<span style="font-size:16px;color:#1E90FF">18650锂电池</span>作为电源,不仅避免了频繁更换电池带来的不便,还减少了废旧电池对环境的污染。</p>
<h1>2、硬件部分</h1>
<h2>2.1电源部分</h2>
<p>电源框图如下图所示:</p>
<p><img src="//image.lceda.cn/oshwhub/d7a36730e2bb4886bbc558f08b0280a4.png" alt="电源框图.png">
<span style="font-size:18px;color:#228B22">设计思路</span></p>
<p>官方设计是使用的两节1.5V干电池,由于需要经常更换电池,废弃电池也会造成污染,因此考虑使用锂电池供电。</p>
<p>经过查阅数据手册得知,STM32G030K6T6供电范围 2V-3.6V、SHT40 1.08V-3.6V、而锂电池满电时能有4V以上,超过了它们允许的最大允许的电源电压,因此需要采用先升压后降压的方式,稳定电压在3.3V。</p>
<p>因为要兼顾整机功耗和成本,经过挑选,<span style="font-size:17px;color:#2E86C1">使用MT3608B做升压、TLV70233做降压。</span></p>
<p>其中,MT3608B在电流小于100mA时的效率约92%。其输出电压是使用电阻分压反馈方式,VOUT=(1+R2/R1) * VREF。手册里写VREF=0.6V,我取R2=91KΩ,R1=13KΩ得到VOUT=4.8V。这里两个分压电阻大一点好,这样流过它们的电流小,它们所耗的功率也会变小。这里的电感4.7uH是按芯片手册来的,建议选择一个等效直流电阻更低的电感,这样也能提高效率。这里的续流二极管必选肖特基二极管,建议用<strong>SS14</strong>就行,我用SS34是因为手头上正好有这个。</p>
<p><img src="//image.lceda.cn/oshwhub/4ce513f007d343d6973a5907af1475a8.png" alt="image.png"></p>
<p><span style="font-size:18px;color:#228B22">Layout注意事项</span></p>
<p>输入端的电容C15要尽可能贴近DC-DC芯片放置,而输出端C14要与芯片地的回路最短。</p>
<p><img src="//image.lceda.cn/oshwhub/ad1d241ca12e4e72b22605ca071602a5.png" alt="image.png"></p>
<p>两个反馈电阻要远离电感,避免高频干扰</p>
<p><img src="//image.lceda.cn/oshwhub/04b6992a1c1d4f48b7a594f44fab971c.png" alt="image.png"></p>
<p>LDO这边,输入和输出的滤波电容需要贴近LDO放置</p>
<p><img src="//image.lceda.cn/oshwhub/068725ed591f4acc997d9444cdeb3c73.png" alt="image.png"></p>
<h2>2.2 温湿度显示</h2>
<p><span style="font-size:18px;color:#228B22">设计思路</span></p>
<p>温湿度分别显示在两片共阴极数码管上。数码管通过三个74HC595移位寄存器控制。移位寄存器的功能框图如下图所示</p>
<p><img src="//image.lceda.cn/oshwhub/efd3040e4f1342809d7c84c93f7fc6d2.png" alt="image.png">
两个数码管的阴极总共是6个阴极,全部连接在其中一个SN74HC595上,通过这一个595芯片,可以指定某一个位导通,同时,两个数码管的阳极,又分别连接在另外两个595芯片上,通过这两个芯片配合,就可以实现单个位显示数据。</p>
<p><img src="//image.lceda.cn/oshwhub/68ccba02114c4832beb40cbe02da064d.png" alt="image.png"></p>
<p><span style="font-size:18px;color:#228B22">Layout注意事项</span></p>
<p>电源滤波电容靠近移位寄存器VCC放置</p>
<p><img src="//image.lceda.cn/oshwhub/fb11a92beb1541ebaad9f7522525f645.png" alt="image.png"></p>
<h2>2.3 STM32外围电路</h2>
<p><span style="font-size:18px;color:#228B22">(1)ADC电压采集</span></p>
<p>使用了两个10KΩ薄膜电阻串联分压,其精度误差为±0.1%,可以基本保证电压采样的准确性。采样点在中间位置,最终需要在程序内乘以2,才能得到近似准确的电池电压。例如由程序ADC转换后的电压为1.92V,则实际电压为3.84V。旁路增加一个100nF电容接地也是为了消除一些干扰。</p>
<p><img src="//image.lceda.cn/oshwhub/593ff430be6144c89a8d293445f6fb45.png" alt="image.png"></p>
<p><span style="font-size:18px;color:#228B22">(2)按键中断唤醒</span></p>
<p>使用一个按键开关实现,一端接STM32的WAKE引脚(需要自己定义),一端接地。当按下后,WAKE引脚从高电平到低电平转换,就产生了下降沿,只需要读到这个下降沿即可执行后续的中断操作,这在后面软件设计部分会细说。
<img src="//image.lceda.cn/oshwhub/4d18183e89d04819897021dcd4aaaa7d.png" alt="image.png"></p>
<h2>2.4 SHT40模块</h2>
<p><span style="font-size:18px;color:#228B22">设计思路</span></p>
<p>立创商城中可以买到<a href="https://item.szlcsc.com/24072575.html?fromZone=s_s__%2522sht40%2520with%2522" target="_blank">SHT40模块</a>,它带有一个滤波电容和传感器,可以发现少了两个I2C通信线上的上拉电阻,我们需要在原理图加上</p>
<p><img src="//image.lceda.cn/oshwhub/3b98d282c0204816b315e680126668a9.png" alt="image.png"></p>
<p><img src="//image.lceda.cn/oshwhub/29b98c74e41b457fbeb9fe15565f2340.png" alt="image.png"></p>
<p><span style="font-size:18px;color:#228B22">原理图注意事项</span></p>
<p><span style="font-size:17px;color:red">请注意原理图中SCL与SDA的位置</span>,一定要根据模块的引脚功能仔细对照!我设计的板子是将传感器朝上放置,如果你想改为朝下放置,则需要改原理图,将引脚镜像对调。</p>
<p>传感器模块各引脚定义如下图所示
<img src="//image.lceda.cn/oshwhub/cef5f89c1a1a4ddd8f3ad54535a93963.jpg" alt="ABEEB11A2AFD5237CEABF774795440C9.jpg"></p>
<p><span style="font-size:18px;color:#228B22">Layout注意事项</span></p>
<p>SHT40这个传感器真的非常非常灵敏,在布局时需要让模块尽量远离发热器件,如充电模块、STM32等,因此我将其布局在了PCB的右上角,底下不铺铜,以减少其他器件散热对它造成的影响。在阻焊颜色选择上,我选用吸热较少的白色,同样是为了减少外界对传感器的影响。</p>
<h1>3、软件部分</h1>
<h2>3.1 软件实现</h2>
<p>部分软件代码参考<a href="https://www.yuque.com/wldz/jlceda/ycxrhmcyxkvomgm1" target="_blank">官方项目文档</a>。
代码已开源并上传到<strong>附件</strong>。</p>
<h3>3.1.1数码管显示数字</h3>
<p>数码管本身不含有任何控制单元,它只是由几个有序排列的LED组成的器件,所以我们需要控制移位寄存器74HC595,通过它来驱动数码管显示数字。74HC595的时序图如下图所示</p>
<p><img src="//image.lceda.cn/oshwhub/1dd08b440d47454aa2bbf227c6235b55.png" alt="image.png"></p>
<p>从时序图可以知道74HC595的使用流程为:</p>
<p>①拉高SCLR(10脚)。如果不用,设计原理图时可以直接拉高。</p>
<p>②控制SI(14引脚)、SCK(11脚)把移位寄存器的值赋好(使用8个上升沿)。</p>
<p>③给RCK(12引脚)一个上升沿。</p>
<p>④拉低G(13脚)。</p>
<p>首先定义函数<code>SN74HC595_Send_Data</code>,它有两个参数:<code>sn_num</code>: 表示选择哪个数码管或设备,值有<code>SN_LED1</code>、<code>SN_LED2</code>和<code>SN_DIG</code>;<code>sendValue</code>: 需要发送的数据。
这里sn_num一共有三种情况,只挑值为SN_LED1的来讨论,其他两个内容都是类似的</p>
<p><img src="//image.lceda.cn/oshwhub/e59cbd742c5a4f7a8db1f7a7046f0f0d.png" alt="image.png"></p>
<p>①选择第一个数码管 (<code>SN_LED1</code>):</p>
<pre><code class="language-c"> if(sn_num == SN_LED1)</code></pre>
<p>②使用<code>for</code>循环遍历8次(因为74HC595是8位移位寄存器):</p>
<pre><code class="language-c"> for(i = 0; i < 8; i++)</code></pre>
<p>③检查<code>sendValue</code>的每一位,如果该位是1,则设置相应的引脚为高电平,否则为低电平:</p>
<pre><code class="language-c"> if(((sendValue << i) & 0x80) != 0)</code></pre>
<p>④产生一个SCLK上升沿,时钟信号用于将数据从串行输入移位到寄存器中:</p>
<pre><code class="language-c"> HAL_GPIO_WritePin(LED1_SCLK_GPIO_Port, LED1_SCLK_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED1_SCLK_GPIO_Port, LED1_SCLK_Pin, GPIO_PIN_SET);</code></pre>
<p>⑤在循环结束后,产生一个RCLK上升沿,用于将移位寄存器中的数据锁存到输出寄存器中:</p>
<pre><code class="language-c"> HAL_GPIO_WritePin(LED1_RCLK_GPIO_Port, LED1_RCLK_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED1_RCLK_GPIO_Port, LED1_RCLK_Pin, GPIO_PIN_SET);</code></pre>
<h3>3.1.2 ADC采集电池电压</h3>
<p>前面硬件设计时提到过,用两个10KΩ薄膜电阻实现分压,STM32采集中间点的电压值,将其转换为ADC值,然后经过一系列运算最终得到电压的准确值。</p>
<p>首先要在STM32CubeMX中配置好ADC引脚,点击PB1引脚,选择ADC1_IN9</p>
<p><img src="//image.lceda.cn/oshwhub/2d4f735d9d2d4266b5c3a84ee07c950c.png" alt="image.png"></p>
<p>来到NVIC中,使能ADC1中断</p>
<p><img src="//image.lceda.cn/oshwhub/f67d9ba84c8e4da7a5cdbfd38e09b841.png" alt="image.png"></p>
<p>再点进Code Generation,按如图所示勾选即可</p>
<p><img src="//image.lceda.cn/oshwhub/aa3ce75db34843f5a580edd64aa639f5.png" alt="image.png"></p>
<p>最后点击GENERATE CODE,打开Keil项目,开始编写ADC代码</p>
<p>这里将ADC代码拆分成三块讲解</p>
<p><img src="//image.lceda.cn/oshwhub/739d22c960ba409a9f219447c04aa907.png" alt="image.png"></p>
<p>①调用HAL库函数,初始化、启动ADC转换,并等待其转换完成</p>
<p>②判断是否获取到数值,由于STM32G030K6T6是12bit ADC,也就是会有2的12次方个ADC数值,ADC数值从0到4095。将其归一化处理,并乘上STM32的输入电压。输入电压我用3.324V是拿万用表测得的,不需要太精确的话写3.3就可以了。归一化后的数据即为一半的电池电压值,变量为Data,比如说1.92V。</p>
<p>③由于需要显示在数码管上,我们把Data放大100倍再乘以2,这样就得到了一个整数,比如384,可以用取十进制数每一位的值的方法,得到三个数:3 8 4,并赋值给<code>device_paramter</code>结构体中的成员Voltage数组
在<code>main.h</code>头文件中,<code>device_paramter</code>结构体定义如下</p>
<pre><code class="language-c">struct DEVICE_PARAMTER
{
volatile uint8_t KeyStatus;
volatile uint8_t sleepStatus;
uint16_t Temp;
uint16_t Humi;
uint8_t Voltage[3];
};</code></pre>
<p>在gpio.c文件中,编写了一个显示电压的函数,这样就方便一键调用了</p>
<pre><code class="language-c">void ShowVoltage(){
SN74HC595_Send_Data(SN_DIG,0xFE);
SN74HC595_Send_Data(SN_LED1,sgh_value[device_paramter.Voltage[0]]|0x80);
SysCtlDelay(1000);
SN74HC595_Send_Data(SN_LED1,0x00); //消影,防止错位
SN74HC595_Send_Data(SN_DIG,0xFD);
SN74HC595_Send_Data(SN_LED1,(sgh_value[device_paramter.Voltage[1]]));
SysCtlDelay(1000);
SN74HC595_Send_Data(SN_LED1,0x00); //消影,防止错位
SN74HC595_Send_Data(SN_DIG,0xFB);
SN74HC595_Send_Data(SN_LED1,sgh_value[device_paramter.Voltage[2]]);
SysCtlDelay(1000);
SN74HC595_Send_Data(SN_LED1,0x00); //消影,防止错位
}
</code></pre>
<h3>3.1.3 模拟I2C(软件I2C)</h3>
<p>由于我在设计时把SCL和SDA画反(目前EDA编辑器里的板子已更正,设计为传感器模块正面向上插入),导致我需要飞线或者用软件I2C解决通信问题。经过查阅大量资料、参考代码,我自行修改并匹配了本项目所使用的传感器和MCU,实现了使用HAL库模拟I2C通信。如果你和我一样无法直接使用硬件I2C,别着急,只需要修改<code>softiic.h</code>内SDA和SCL对应的GPIO即可。</p>
<p><img src="//image.lceda.cn/oshwhub/cb2a448ff7614b4296de18ff4aa24d00.png" alt="image.png"></p>
<p><span style="font-size:17px;color:red">由于软件I2C篇幅较长,所以放到第3.2节细讲 </span></p>
<h3>3.1.4 中断唤醒</h3>
<p><span style="font-size:18px;color:#458FC3">(1)STM32CubeMX参数设置</span></p>
<p>打开STM32CubeMX,设置PB5为GPIO_EXTI5,GPIO模式选择“下降沿触发检测的外部中断”,GPIO上拉,命名标签为WAKE_KEY</p>
<p><img src="//image.lceda.cn/oshwhub/e2305d0df99d48faa91b1090bd424ad6.png" alt="image.png"></p>
<p>在NVIC中,设置EXTI line 4 to 15 interrupts使能并将优先级设为1;然后进到Code generation中,勾选对应的生成代码等复选框</p>
<p><img src="//image.lceda.cn/oshwhub/71db7f77a5794cf4bb67a21835cfa70e.png" alt="image.png">
<img src="//image.lceda.cn/oshwhub/2a0fb7afe40d4a8f94f2e4f41a7ba60b.png" alt="image.png"></p>
<p>在Timers- TIM14中使能TIM14 全局中断</p>
<p><img src="//image.lceda.cn/oshwhub/482fd0dd4f4147cd8f93349a9fdf30ac.png" alt="image.png"></p>
<p>OK,点击右上角的生成代码,打开Keil,编辑代码。</p>
<p><span style="font-size:18px;color:#458FC3">(2)中断代码</span></p>
<p>①本段程序在<code>tim.c</code>中,是回调函数。当定时器被触发时,HAL库自动调用该段函数。
函数主要做了两件事:显示温湿度、显示电池电压。变量<code>flag</code>用于计时以及判断是否该调整休眠标志<code>sleep_flag</code>。</p>
<pre><code class="language-c">void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM14)
{
HAL_TIM_Base_Stop_IT(&htim14);
updata_flag++;
if(updata_flag <= 1000)
{
ShowNum(1,1,(device_paramter.Temp/100));
ShowNum(1,2,(device_paramter.Temp / 10 % 10));
ShowNum(1,3,device_paramter.Temp%10);
ShowNum(2,1,(device_paramter.Humi/100));
ShowNum(2,2,(device_paramter.Humi / 10 % 10));
ShowNum(2,3,device_paramter.Humi%10);
}
else if(updata_flag <= 2000)
{
ShowVoltage();
}
else
{
updata_flag = 0;
sleep_flag++;
}
__HAL_TIM_SetCounter(&htim14,0);
if(sleep_flag >= 1)
{
sleep_flag = 0;
device_paramter.sleepStatus = 1;
SN74HC595_Send_Data(SN_DIG,0xFF);
SN74HC595_Send_Data(SN_LED1,0x00);
SN74HC595_Send_Data(SN_LED2,0x00);
}
else{
HAL_TIM_Base_Start_IT(&htim14);
}
}
}</code></pre>
<p>②本段程序在<code>main.c</code>的<code>while(1)</code>循环中,这里把SHT40温湿度采集与ADC电压采集的代码删掉,只保留该节要讲的内容。</p>
<p>循环一直判断按键的状态,当读取到PB5引脚为低电平(GPIO_PIN_REST)时,进入下一级循环,然后执行<code>HAL_TIM_Base_Start_IT()</code>启动定时器,它会自动调用上面的<code>HAL_TIM_PeriodElapsedCallback</code>,待回调函数内执行完毕,睡眠标志到来时,将退出回调函数,回到这一段代码,继续把休眠标志和按下标志清除掉,MCU进入休眠,可以进行下一次唤醒。</p>
<pre><code class="language-c">
if(device_paramter.KeyStatus == KEY_SHAKE_STATE)
{
HAL_Delay(10);
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_5) == GPIO_PIN_RESET)
{
while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_5) == GPIO_PIN_RESET);
/*
SHT40采集温湿度
*/
/*
ADC采集电压
*/
HAL_TIM_Base_Start_IT(&htim14); //开始定时器,显示数据
device_paramter.sleepStatus = 0; //清除休眠标志
device_paramter.KeyStatus = KEY_NO_PRESS; //清除按下标志
}
}
else if(device_paramter.sleepStatus == 1) //显示结束,进入休眠
{
HAL_SuspendTick(); //暂停滴答定时器,防止通过滴答定时器中断唤醒
HAL_PWR_EnterSTOPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI); //进入停止模式
}
</code></pre>
<h2>3.2 软件I2C详解</h2>
<h3>3.2.1 为什么要用软件I2C?</h3>
<p>I2C通信协议是本项目的关键所在,SHT40支持I2C通信,只有搞懂I2C通信的全过程,才能给SHT40传感器发送读数据命令、正确接收温湿度数据。据说STM32的硬件I2C存在bug,再加上本人被迫需要学习软件I2C,因此花了一天的时间来学习调试,在此记录、解析一下I2C的通信过程及其如何实现获取SHT40的温湿度数据的。</p>
<h3>3.2.2 I2C通信原理解释</h3>
<p><span style="font-size:18px;color:#458FC3">(1)空闲状态</span></p>
<p>当SCL和SDA都为高电平时</p>
<p><img src="//image.lceda.cn/oshwhub/f49c4d4539ba4ae280e26325d76a9100.png" alt="image.png">
<span style="font-size:18px;color:#458FC3">(2)起始、结束条件</span></p>
<p>起始条件:当SCL为高电平时,SDA有一个下降沿</p>
<p>结束条件:当SCL为高电平时,SDA有一个上升沿</p>
<p><img src="//image.lceda.cn/oshwhub/5f66459d97ef4063871a48ae95ff4aa2.png" alt="image.png"></p>
<p><span style="font-size:18px;color:#458FC3">(3)从机地址、寄存器地址</span></p>
<p>I2C通信就如同广播找人一样,你叫到某个人的名字(从机的地址),它才会给你回应。因此,需要在开始条件后紧跟着发送一个从机地址。从机地址由7bit组成,第8位是读写标志位,0表示写,1表示读</p>
<p><img src="//image.lceda.cn/oshwhub/74ed149d8241475a8fcddff855c927b4.png" alt="image.png"></p>
<p>举例:当你广播找人,找到了那个人(从机),他回应你了(ACK),你要他口袋里(寄存器地址)的钥匙(数据)
<span style="font-size:18px;color:#458FC3">(4)字节格式</span>
开始条件之后,每8个bit为一个字节传输,当SCL为高电平时,SDA也是高电平,此时代表逻辑1;SCL为高电平,SDA是低电平时,代表逻辑0。
<img src="//image.lceda.cn/oshwhub/ae4056680f464d6dbd25bee6db7f7a98.png" alt="image.png"></p>
<p><span style="font-size:18px;color:#458FC3">(5)应答位</span></p>
<p>I2C的数据都是以8bit传送的,发送器每发送一个字节,就在时钟脉冲9期间释放数据线SDA,由接收器反应一个应答信号。当应答信号为低电平时,规定为有效应答位(ACK),表示接收器已经成功接收该字节。当应答信号为高电平时,规定为非应答位(NACK),表示接收器接收该字节没有成功。</p>
<p>举个例子:这就像是你给别人说你的电话号码,你一次性把11位电话号说给他,他可能一下子记不住,你分成三段,第一段说139,他说 好的(有效应答位ACK);第二段你说1234,他说 嗯(有效应答位ACK);第三段你说5678,这时一辆大车鸣笛经过,淹没了你的声音,他就没听清,没回应你(非应答位NACK),此时你看他没反应,你就要再说一遍5678,他回答好的(有效应答位ACK)。</p>
<h3>3.2.3 I2C通信过程</h3>
<p><span style="font-size:18px;color:#458FC3">(1)主机写入数据的流程</span></p>
<p><img src="//image.lceda.cn/oshwhub/595c68ecaeea41babeacd82a4d441902.png" alt="image.png"></p>
<p><span style="font-size:18px;color:#458FC3">(2)主机读取数据的流程</span></p>
<p><img src="//image.lceda.cn/oshwhub/7434aab7cb504071a7be14b4d3b9fb91.png" alt="image.png"></p>
<h3>3.2.4 I2C代码实现方法</h3>
<p>有了上述理论基础,就可以开始编写代码了。这里我单独创建了一个C源文件叫<code>softiic.c</code>,下面将逐个函数讲解其作用。</p>
<p><span style="font-size:18px;color:#458FC3">(1)使用HAL库时自定义的delay_us函数</span></p>
<p>由于HAL库官方给的HAL_Delay是以ms级别的,我们要产生模拟I2C信号需要有延迟,通常是5us。<a href="https://shequ.stmicroelectronics.cn/thread-634303-1-1.html" target="_blank">ST论坛</a>有大佬给出了解决方法,这里就直接照搬了。注意修改时钟主频为STM32G030K6T6的64MHz</p>
<pre><code class="language-c">#define CPU_FREQUENCY_MHZ 64 // STM32时钟主频 MHz
void delay_us(__IO uint32_t delay)
{
int last, curr, val;
int temp;
while (delay != 0)
{
temp = delay > 900 ? 900 : delay;
last = SysTick->VAL;
curr = last - CPU_FREQUENCY_MHZ * temp;
if (curr >= 0)
{
do
{
val = SysTick->VAL;
}
while ((val < last) && (val >= curr));
}
else
{
curr += CPU_FREQUENCY_MHZ * 1000;
do
{
val = SysTick->VAL;
}
while ((val <= last) || (val > curr));
}
delay -= temp;
}
}</code></pre>
<p><span style="font-size:18px;color:#458FC3">(2)配置切换SDA引脚输入或输出模式的函数</span></p>
<p>使用HAL库的配置方法。注意,输出模式为开漏(OD)输出</p>
<pre><code class="language-c">static void SDA_OUT(void){
GPIO_InitTypeDef SOFT_IIC_GPIO_STRUCT;
SOFT_IIC_GPIO_STRUCT.Mode = GPIO_MODE_OUTPUT_OD;
SOFT_IIC_GPIO_STRUCT.Pin = SDA_PIN;
SOFT_IIC_GPIO_STRUCT.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(SDA_PORT, &SOFT_IIC_GPIO_STRUCT);
}
static void SDA_IN(void)
{
GPIO_InitTypeDef SOFT_IIC_GPIO_STRUCT;
SOFT_IIC_GPIO_STRUCT.Mode = GPIO_MODE_INPUT;
SOFT_IIC_GPIO_STRUCT.Pin = SDA_PIN;
SOFT_IIC_GPIO_STRUCT.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(SDA_PORT, &SOFT_IIC_GPIO_STRUCT);
}
</code></pre>
<p><span style="font-size:18px;color:#458FC3">(3)I2C延迟时间定义</span></p>
<p>前面有了自定义函数<code>delay_us</code>,这里我们填5,意味着每次调用函数,延迟5us</p>
<pre><code class="language-c">void IIC_Delay(void) {
delay_us(5);
}</code></pre>
<p><span style="font-size:18px;color:#458FC3">(4)I2C开始信号</span></p>
<p>先将SDA设置为输出模式,SDA和SCL同时为高电平(空闲状态),经过一点延时,SDA拉低,SCL过5us后再拉低,完成开始信号的设置。</p>
<pre><code class="language-c">void IIC_Start(void) {
SDA_OUT();
SDA_HIGH();
SCL_HIGH();
IIC_Delay();
SDA_LOW();
IIC_Delay();
SCL_LOW();
}</code></pre>
<p><span style="font-size:18px;color:#458FC3">(5)I2C结束信号</span></p>
<p>先将SDA设置为输出模式,SDA和SCL同时为低电平,经过一点延时,SCL先拉高,SDA过5us后再拉高,完成结束信号的设置。</p>
<pre><code class="language-c">void IIC_Stop(void) {
SDA_OUT();
SCL_LOW();
SDA_LOW();
IIC_Delay();
SCL_HIGH();
IIC_Delay();
SDA_HIGH();
IIC_Delay();
}</code></pre>
<p><span style="font-size:18px;color:#458FC3">(6)应答位</span></p>
<p>这里主要是IIC_Wait_Ack函数需要讲一下,IIC_Ack和IIC_NAck是模拟主机发送应答或非应答信号,看着前面I2C的定义图应该很清楚,这里就不逐行啰嗦了。</p>
<p>IIC_Wait_Ack函数中,要将SDA设置为输入模式,以便于读取SDA信号线上电平的变化,其中<code>READ_SDA()</code>是由宏定义为 <code>HAL_GPIO_ReadPin(SDA_PORT, SDA_PIN)</code>,如果高电平,则返回值为1;若低电平,返回值为0。回顾一下,ACK是要求从机有一个拉低SDA的操作,即<code>READ_SDA()</code>返回值为0,方可跳出while循环,继续下面拉低SCL的操作。</p>
<p>在循环内,<code>wait</code>变量是用来判断是否超时,只要超过200个周期,就判定超时,停止I2C通信.</p>
<pre><code class="language-c">uint8_t IIC_Wait_Ack(void)
{
uint8_t wait;
SDA_IN();
IIC_Delay();
SCL_HIGH();
IIC_Delay();
while (READ_SDA())
{
wait++;
if (wait > 200)
{
IIC_Stop();
return 1;
}
}
SCL_LOW();
return 0;
}
void IIC_Ack(void) {
SCL_LOW();
SDA_OUT();
SDA_LOW();
IIC_Delay();
SCL_HIGH();
IIC_Delay();
SCL_LOW();
}
void IIC_NAck(void) {
SCL_LOW();
SDA_OUT();
SDA_HIGH();
IIC_Delay();
SCL_HIGH();
IIC_Delay();
SCL_LOW();
}</code></pre>
<p><span style="font-size:18px;color:#458FC3">(7)I2C发送一个字节</span></p>
<p>这里就不一行行去解读了,主要看一下for循环里面的内容在干什么。</p>
<p>①因为一个字节包含8位,而I2C通信中数据是以位为单位传输的。因此,<code>for (uint8_t i = 0; i < 8; i++)</code> ,从0到7遍历一个字节的每一位。</p>
<p>②将<code>byte</code>向左移动<code>i</code>位,这样原本的第<code>i</code>位就移动到了最高位(即第7位,对应于十六进制的<code>0x80</code>)。然后,使用<code>&</code>操作符与<code>0x80</code>进行按位与操作。如果结果为非零(即<code>true</code>),说明<code>byte</code>的第<code>i</code>位是1;如果结果为0(即<code>false</code>),则第<code>i</code>位是0。</p>
<p>③设置SDA线的电平:根据上一步的结果,通过<code>SDA_HIGH()</code>和<code>SDA_LOW()</code>宏或函数来设置SDA线的电平。如果当前位是1,则调用<code>SDA_HIGH()</code>将SDA线设置为高电平;如果当前位是0,则调用<code>SDA_LOW()</code>将SDA线设置为低电平。这样,就按照<code>byte</code>变量的值,一位一位地将数据通过SDA线发送出去。</p>
<pre><code class="language-c">void IIC_Send_Byte(uint8_t byte) {
SDA_OUT();
SCL_LOW();
for (uint8_t i = 0; i < 8; i++) {
if ((byte << i) & 0x80) {
SDA_HIGH();
} else {
SDA_LOW();
}
IIC_Delay();
SCL_HIGH();
IIC_Delay();
SCL_LOW();
IIC_Delay();
}
}</code></pre>
<p><span style="font-size:18px;color:#458FC3">(8)I2C接收一个字节</span></p>
<p>与发送一字节类似,可以对照着来看,这里就不再啰嗦了。</p>
<pre><code class="language-c">uint8_t IIC_Read_Byte(uint8_t ack) {
uint8_t byte = 0;
SDA_IN();
for (uint8_t i = 0; i < 8; i++) {
SCL_LOW();
IIC_Delay();
SCL_HIGH();
byte <<= 1;
if (READ_SDA()) {
byte |= 0x01;
}
IIC_Delay();
}
return byte;
}</code></pre>
<h3>3.2.5 实现与温湿度传感器的I2C通信</h3>
<p>这一块主要包含<code>softiic.c</code>中的3个函数,以及一个在<code>main.c</code>中调用的函数块。</p>
<p>SHT40的地址和发送的命令在softiic.h中先进行了宏定义:</p>
<pre><code class="language-c">#define SHT40_ADDRESS 0x44 // SHT40的I2C地址
#define SHT40_COMMAND_MEASURE_HIGH_PRECISION 0xFD //要发送的命令 0XFD</code></pre>
<p><span style="font-size:18px;color:#458FC3">(1)<code>SHT40_Start_Measurement</code>开始测量温湿度函数</span></p>
<p>其实是把下面的写入命令函数加了延时10ms,让SHT40有时间获取到温湿度值。可以合并进写函数(2),但个人认为那样不直观。</p>
<pre><code class="language-c">void SHT40_Start_Measurement(void) {
Soft_IIC_Write_Command(SHT40_ADDRESS, SHT40_COMMAND_MEASURE_HIGH_PRECISION);
HAL_Delay(10); // 根据数据手册说明,延时10ms
}</code></pre>
<p><span style="font-size:18px;color:#458FC3">(2)<code>Soft_IIC_Write_Command</code> 发送命令函数</span></p>
<p>设备地址是0x44,转化为7位二进制为 <code>1000100</code>,由于这里是写入,读写标志位为0(写),因此发送的这个字节为 <code>10001000</code>。</p>
<p>“命令”为0xFD,这可以在数据手册中查到,对应的返回值是6字节数据,第1、2字节是温度数据,第3字节是CRC校验和;第4、5是湿度数据,第6位是校验和。这些数据都是高精度的。</p>
<p><img src="//image.lceda.cn/oshwhub/1d8da8d57d894da58d7e74117fc8cef3.png" alt="image.png"></p>
<p>根据原理部分讲的,在发送完地址后就要发送“寄存器地址”,而这里有个小坑,我最开始没绕明白。通常在网上搜到的I2C代码其写入函数参数有:设备地址、寄存器地址、写入的数据,而SHT40这的“命令”其实就是寄存器地址,只是这个寄存器是只读的。当SHT40接收到寄存器地址后,它就会去读取温湿度数据,并存在它的寄存器里,等待主机的读取命令。这里不需要给SHT40写入数据,也就是发送完寄存器地址后,等待应答然后关闭I2C传输,再等待0.01s,开始读取即可。</p>
<pre><code class="language-c">void Soft_IIC_Write_Command(uint8_t deviceAddr, uint8_t command) {
IIC_Start();
IIC_Send_Byte(deviceAddr << 1); // 发送设备地址和写位
IIC_Wait_Ack();
IIC_Send_Byte(command); // 发送命令
IIC_Wait_Ack();
IIC_Stop();
}</code></pre>
<p><span style="font-size:18px;color:#458FC3">(3)<code>SHT40_Read_Measurement</code>读取温湿度数据函数</span></p>
<p>这里给SHT40发送读位,即读写标志位变为1,发送的字节为<code>10001001</code>。</p>
<p>然后等SHT40回应,并释放SDA线,让SHT40发送它刚刚读到的温湿度数据,并存到data数组中。</p>
<pre><code class="language-c">void SHT40_Read_Measurement(uint8_t* data, uint8_t length) {
IIC_Start();
IIC_Send_Byte((SHT40_ADDRESS << 1) | 0x01); // 发送设备地址和读位
IIC_Wait_Ack();
for (uint8_t i = 0; i < length-1; i++) {
*data = IIC_Read_Byte(i < (length - 1)); // 读取数据并发送应答信号
data++;
IIC_Ack();
}
*data = IIC_Read_Byte(0);
IIC_NAck();
IIC_Stop();
}</code></pre>
<p><span style="font-size:18px;color:#458FC3">(4)main.c中获取温湿度数据的实现</span></p>
<p>调用刚刚写好的函数<code>SHT40_Start_Measurement</code>和<code>SHT40_Read_Measurement</code>,这里的readData数组要提前初始化好,长度为6。</p>
<p>然后根据数据手册里的伪代码</p>
<p><img src="//image.lceda.cn/oshwhub/35d8d66917664fd584f6b757229a0372.png" alt="image.png"></p>
<p>把获取到的数值换算成能读得懂的数,最后放大温湿度,以便数码管显示。</p>
<pre><code class="language-c">/* SHT40采集温湿度*/
SHT40_Start_Measurement();
SHT40_Read_Measurement((uint8_t*)readData,6);
Temperature = (1.0 * 175 * (readData[0] * 256 + readData[1])) / 65535.0 - 45;
Humidity = (1.0 * 125 * (readData[3] * 256 + readData[4])) / 65535.0 - 6.0;
device_paramter.Temp = Temperature * 10; //放大温湿度
device_paramter.Humi = Humidity * 10;</code></pre>
<p>接下来,编译、烧录、插电开机~用示波器捕捉并解码收发数据过程</p>
<p><img src="//image.lceda.cn/oshwhub/1411c9ee0e3e4fbd9823faab2e452744.png" alt="image.png"></p>
<p>当你捕捉到这些跳动的高低电平那一刻,成就感瞬间爆棚啊~这就是电子的魅力~</p>
<h2>3.3 优化程序降低功耗的方法</h2>
<p>STM32G0系列有四种休眠模式;</p>
<p>● 低功耗运行模式(降低CPU频率,系统仍在运行)</p>
<p>● 睡眠模式(系统进入睡眠,任意中断/事件唤醒)</p>
<p>● 停止模式(系统进入停止,支持任意外部中断和RTC闹钟唤醒)</p>
<p>● 待机模式(系统进入待机,支持RTC闹钟唤醒,WKUP、NRST引脚唤醒以及IWDG复位唤醒,打开了LSI和LSE)</p>
<p>如果按照官方文档的代码,让温湿度计进入<strong>睡眠模式</strong>,经测量<span style="font-size:17px;color:red">仍有2.6mA的电流。</span></p>
<p><img src="//image.lceda.cn/oshwhub/584e9a08aaad4a4ea812615a6e1abe23.png" alt="image.png"></p>
<p><img src="//image.lceda.cn/oshwhub/c1d056d5960b41a799ac41662a450cfb.jpg" alt="IMG_20240717_220545.jpg"></p>
<p>而如果设置成<strong>停止模式</strong>,仍然可以用中断唤醒,但此时电流<span style="font-size:17px;color:red">降低至0.21mA !!</span></p>
<p><img src="//image.lceda.cn/oshwhub/36b835d4fbd7487ab98e0011cddae313.png" alt="image.png"></p>
<p><img src="//image.lceda.cn/oshwhub/0cb87bd082144ab6b355da3e8e3d7cdc.jpg" alt="IMG_20240717_220141.jpg"></p>
<h2>3.4 程序烧录步骤</h2>
<p>(1)从附件中下载SHT40_Project.zip,并解压,进入MDK-ARM文件夹,用Keil打开SHT40_Project.uvprojx</p>
<p>(2)将ST-LINK针对针地与温湿度传感器电路板上的调试口相连接</p>
<p>(3)点击编译(Build),点击下载(Download)</p>
<p><img src="//image.lceda.cn/oshwhub/cb4d5ff130154ad1b3ff82105bc7c007.png" alt="image.png"></p>
<p>(4)断开ST-LINK,在电路板背面放入18650锂电池,注意一定不要接反!(有弹簧一侧为负极)</p>
<p>(5)正常来说,上电后数码管不亮,按一下按钮,两个数码管同时显示温湿度5秒,然后左侧数码管显示电池电压4秒,然后进入休眠。</p>
<h1>4 故障排除</h1>
<p><span style="font-size:19px;color:red">故障1:上电后只有第一次温湿度读数,而后续读数错误。</span></p>
<p><span style="font-size:19px;color:orange">故障描述:</span>插电,发现只显示读出第一次的温湿度数值,之后进入while循环的数值就读不到了,数码管显示0.0 9.0。</p>
<p><span style="font-size:19px;color:orange">解决过程:</span>经过一番摸索,在我用示波器表笔接SCL,准备再次查看波形时,发现无波形变化,遂确认是硬件问题而非软件BUG。于是我用万用表电阻档再次测量3.3V与SCL、SDA之间的电阻,发现它们均不为4.7KΩ(上拉至3.3V的电阻值),于是拆下来两个电阻挨个测量,最后发现SCL的上拉的电阻损坏</p>
<p><span style="font-size:19px;color:orange">故障原因:</span><span style="font-size:16px;color:#1E90FF">上拉电阻损坏。</span></p>
<p><span style="font-size:19px;color:red">故障2:数码管显示0.0和9.0</span></p>
<p><span style="font-size:19px;color:orange">故障描述:</span>上电后,数码管读不到正确的温湿度值,不同于故障1还有一次可读的数值。</p>
<p><span style="font-size:19px;color:orange">解决过程:</span>显示0.0和9.0是由于初始化的数据接收数组内元素都为0,而通信读取温湿度过程中数组内数值并未改变,代入温湿度的计算公式得到了固定值0.0和9.0。可用测量通断挡位,一支表笔接传感器模块插口,一支表笔接STM32对应引脚,分别测试SCL、SDA、VCC、GND判断是否接触良好。若接触良好,则有可能传感器损坏。</p>
<p><span style="font-size:19px;color:orange">故障原因:</span><span style="font-size:16px;color:#1E90FF">接触不良或温湿度传感器损坏</span></p>
<p><span style="font-size:19px;color:red">故障3:想再次烧录代码,Keil提示invalid rom table</span></p>
<p><span style="font-size:19px;color:orange">故障描述:</span>已经烧录好我在附件里的源代码,想改动一点自己的东西,再烧录发现Keil报错。</p>
<p><span style="font-size:19px;color:orange">解决过程:</span>由于有让MCU进入休眠模式的代码,在上电后它会立即进入休眠模式,以保持低功耗运行。此时可以先插好ST-LINK,在Keil程序编译好,然后按下唤醒按钮,在数码管仍在显示数值的过程中,手快速地点击Keil的下载按钮即可。
.
<span style="font-size:19px;color:orange">故障原因:</span><span style="font-size:16px;color:#1E90FF">MCU进入休眠模式</span></p>
<h1>5 项目属性</h1>
<p>首次公开,在官方原理图基础上增加锂电池充电管理电路、DC-DC升压以及LDO降压电路,以及优化PCB布局,编写软件I2C等代码。未在别的比赛中获奖。</p>
<h1>6 开源协议</h1>
<p>GPL3.0</p>
<h1>7 大赛LOGO验证</h1>
<p>温湿度计电路板整体外观展示
<img src="//image.lceda.cn/oshwhub/1477be0bda8f4354829d7978ba8bd863.jpg" alt="IMG_20240718_214156.jpg"></p>
<p>显示温度、湿度状态展示
<img src="//image.lceda.cn/oshwhub/70cd8d5a63364b05b3982e83c3cbdcce.jpg" alt="IMG_20240718_214309.jpg"></p>
<p>显示电池电压
<img src="//image.lceda.cn/oshwhub/b5c607f509d9436e8aa79610a2e4f503.jpg" alt="IMG_20240718_214300.jpg"></p>
<h1>8 演示视频</h1>
<p>演示视频可在附件查看,也同步上传至B站:</p>
<p><a href="https://www.bilibili.com/video/BV1C4421Z7rL/" target="_blank">https://www.bilibili.com/video/BV1C4421Z7rL/</a></p>
<h1>写在后面</h1>
<p>第二次参加训练营啦,这次借着温湿度计训练营一同参加了第九届立创电赛。在设计硬件、调试软件的过程中真的能学到很多东西!查资料非常重要,不论是国内国外的资料,在查的过程中也能逐渐理清知识脉络。</p>
<p>在这里要感谢<a href="https://sensirion.com/cn/" target="_blank">@盛思锐</a>,温湿度传感器很好用,数据手册内容内容非常全面。</p>
<h1>参考</h1>
<ol>
<li><a href="https://blog.csdn.net/qq_38575895/article/details/127641344" target="_blank">https://blog.csdn.net/qq_38575895/article/details/127641344</a></li>
<li><a href="https://blog.csdn.net/qlexcel/article/details/117159467" target="_blank">https://blog.csdn.net/qlexcel/article/details/117159467</a></li>
<li><a href="https://www.bilibili.com/video/BV1dg4y1H773/?spm_id_from=333.337.search-card.all.click&vd_source=b0a6c01daa3f29bd494dc070c1c1bf14" target="_blank">https://www.bilibili.com/video/BV1dg4y1H773/?spm_id_from=333.337.search-card.all.click&vd_source=b0a6c01daa3f29bd494dc070c1c1bf14</a></li>
<li><a href="https://www.yuque.com/wldz/jlceda/ul1wcz7n5dgt6s60" target="_blank">https://www.yuque.com/wldz/jlceda/ul1wcz7n5dgt6s60</a></li>
</ol>
评论(19)