You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

405 lines
15 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 1. ADC
## 1.1. ADC基本原理
什么是ADCAnalog-to-Digital Converter模拟信号转换为数字信号通过测量模拟信号的电压把电压值转换成数字进行后续的操作。
ADC的本质是通过测量电压值来获得被测信号的电压幅值并保存成一个数值。通过这种机制不仅可以测电压还可以测电阻、电容、电感、压力、温度等只要这些被测量通过一个传感器可以和电压之间产生某种函数关系。
对电压进行测量后保存到的有效数据的位数被叫做精度例如8位精度可以有256个值10位精度的有1024个值该MCU的ADC精度是12位的有4096个值
另外ADC中还有个重要的概念叫做参考电压也就是测量电压的基准。在该芯片中内部基准电压叫做 Vrefint经查阅资料应该是2.048V也就是说ADC最高能检测的电压是2048V。如果按照12位精度采样那得到的分辨率是0.0005V。
> 问题如果电压高于2.048V应该如何进行检测?
![image-20241013171356277](./img/image-20241013171356277.png)
以上是ADC的内部框图我们关注主要的是Vref、GPIO、ADC中断、DMA请求、ADCCLK。目前我们使用内部基准通过GPIO采集电压然后使用轮询模式读取电压值。其他方式还包括中断方式和DMA方式。
开发板内部有两个ADC的测试
![image-20241013171711301](./img/image-20241013171711301.png)
一个是温度热电偶,另外一个是电位器。具体使用我们在后面说明。
扩展阅读:
[ADC原理](https://www.bilibili.com/video/BV1BV4y1V7nE/?spm_id_from=333.337.search-card.all.click&vd_source=3c8e333d6657680a469ddf0238f01d6a)
## 1.2. PCM
PCM是一种通过波形幅值测量和对信号进行数字化的一种方案
![image-20241013172118023](./img/image-20241013172118023.png)
[PCM视频讲解](https://www.bilibili.com/video/BV1yb4y1M7a2/?spm_id_from=333.337.search-card.all.click&vd_source=3c8e333d6657680a469ddf0238f01d6a)
模拟信号通过间隔时间采样得到每个点的幅值然后进行保存和处理。计算机中常见的wav格式就是PCM采样后的文件。衡量PCM的指标有两个采样率和精度ADC精度。一般采样率越高越好信号的失真越小但是存储的空间越大。一般来说采样率不能低于最高频率的两倍因此以前常见的音频采样率是44k声音最高20k目前还有96k以上的高保真。
## 1.3. 实例NTC温度测试
任务描述通过NTC测量环境温度并通过串口打印出来。
NTC 又叫热敏电阻温度的变化会改变其电阻值。通过这一特性结合ADC就可以测量温度。
开发版的NTC是如下电路
![image-20241013183636425](./img/image-20241013183636425.png)
是接在PA4上面的一个电阻通过10K的R6形成分压等效电路如下
![alt text](img/ntc.drawio.png)
当NTC的电阻值变化的时候PA4这个分压点检测到的电压会随之变化。当对这个电压进行ADC转换后可以反向计算NTC的电阻值然后根据NTC的电阻和温度的关系可以计算出温度。
NTC是附件与开发板这样连接
![ad78bbf910c2747701476e350990a89](./img/ad78bbf910c2747701476e350990a89.jpg)
### 1.3.1. 配置
在引脚配置中选择PA4然后选择ADC1_IN4
![image-20241013185308417](./img/image-20241013185308417.png)
设置ADC参数
![image-20241013191944257](./img/image-20241013191944257.png)
Continuous Conversion Mode设为Enable使ADC转换持续进行不需要每次获取之前手动触发转换ADC_Regular_ConversionMode -> Rank -> Sampling Time设为239.5 Cycles最长采样时间可以获得更稳定的转换结果。
打开串口2
在菜单project Properties -> C/C++ Build -> Settings -> Tool Settings -> MCU Settings勾选Use float with printf ...
![image-20241013192823116](./img/image-20241013192823116.png)
这样printf就可以打印小数。
### 1.3.2. 代码编写
在 inc 目录新建一个头文件 NTC.h因为后面还会使用到NTC的一些功能
```h
#ifndef INC_NTC_H_
#define INC_NTC_H_
#include "main.h"
#include "math.h"
extern float NTC_Temperature;
float ADC2Resistance(uint32_t adc_value);
float resistance2Temperature(float R1);
#endif /* INC_NTC_H_ */
```
在 src 目录新建 NTC.c 文件这个文件是NTC转换成温度的一些函数。
```c
/**
* @brief 通过ADC值计算NTC电阻
*
* @param adc_value ADC原始值 [0, 4095]
* @retval 返回NTC电阻值浮点数类型单位Ω
*/
#include "NTC.h"
float NTC_Temperature;
float ADC2Resistance(uint32_t adc_value) {
return (adc_value / (4096.0f - adc_value)) * 10000.0f;
}
/**
* @brief 通过NTC电阻反推温度
*
* @param R1 NTC电阻值
* @retval 返回温度float类型单位摄氏度
*/
float resistance2Temperature(float R1) {
float B = 3950.0f;
float R2 = 10000.0f;
float T2 = 25.0f;
return (1.0 / ((1.0 / B) * log(R1 / R2) + (1.0 / (T2 + 273.15))) - 273.15);
}
```
以下是 main.c文件的内容
在 USER CODE BEGIN Includes 代码段引入头文件
```c
/* USER CODE BEGIN Includes */
#include <string.h>
#include <stdio.h>
#include "NTC.h"
/* USER CODE END Includes */
```
在 USER CODE BEGIN 2 代码段加入如下内容:
```c
/* USER CODE BEGIN 2 */
char send_buf[50] = { 0 };
uint16_t adc_result = 0;
float ntc_resistance = 0.0f;
float temperature = 0.0f;
HAL_ADC_Start(&hadc1); // 开始连续ADC转换
HAL_Delay(500); // 等待ADC稳定
/* USER CODE END 2 */
```
完善主函数的循环:
```c
/* USER CODE BEGIN WHILE */
while (1) {
adc_result = HAL_ADC_GetValue(&hadc1); // 获取ADC值
ntc_resistance = ADC2Resistance(adc_result); // 获取ADC值
temperature = resistance2Temperature(ntc_resistance); // 获取ADC值
sprintf(send_buf, "阻值%.1f Ω,温度: %.2f ℃\r\n", ntc_resistance,
temperature); // 将变量打印为字符串
HAL_UART_Transmit(&huart2, (uint8_t*) send_buf, strlen(send_buf), 10); // 通过串口2发送
HAL_Delay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
```
### 1.3.3. 代码分析
# 2. I2C
[I2C 温湿度传感器](https://www.bilibili.com/video/BV1QN411D7ak/?spm_id_from=333.788&vd_source=3c8e333d6657680a469ddf0238f01d6a)
## 2.1. 设置
1. 先新建一个工程,取名为 humidity
2. 设置调试模式为 Serial Wire
3. 打开串口2
4. 查看开发板19号外设使用PB6/PB7
![image-20241015151124859](./img/image-20241015151124859.png)
在引脚设置中点击PB6看到使用的是I2C1
![image-20241015151052757](./img/image-20241015151052757.png)
5. 在Connectivity中找到I2C1设置I2C模式为 I2C
![image-20241015151355494](./img/image-20241015151355494.png)
6. 在Project Manager中设置为每个外设使用独立的 c/.h 文件:
![image-20241015151501806](./img/image-20241015151501806.png)
7. 按照上个例子设置printf使用浮点
## 2.2. 编码
inc目录建立一个新文件aht20.h
```h
#ifndef __DHT20_H__
#define __DHT20_H__
#include "i2c.h"
// 初始化AHT20
void AHT20_Init(void);
// 获取温度和湿度
void AHT20_Read();
#endif
```
src 目录建立一个新文件 aht20.c
```c
#include <aht20.h>
#define AHT20_ADDRESS 0x70
/**
* 查看状态字是否校准
*/
void AHT20_Init(void) {
uint8_t read_buf = 0;
// 等待DHT20上电稳定
HAL_Delay(40);
HAL_I2C_Master_Receive(&hi2c1, AHT20_ADDRESS, &read_buf, 1, 10);
if ((read_buf & 0x08) != 0x08) {
// 状态字没有校准,发送出初始化命令
uint8_t send_buf[3] = { 0xBE, 0x08, 0x00 };
HAL_I2C_Master_Transmit_DMA(&hi2c1, AHT20_ADDRESS, send_buf, 3);
HAL_Delay(100);
}
}
/**
*
*/
void AHT20_Read(float *temp, float *hum) {
uint8_t sendBuffer[3] = { 0xAC, 0x33, 0x00 };
uint8_t readBuffer[6];
HAL_I2C_Master_Transmit(&hi2c1, AHT20_ADDRESS, sendBuffer, 3, HAL_MAX_DELAY);
HAL_Delay(75);
HAL_I2C_Master_Receive(& hi2c1, AHT20_ADDRESS, readBuffer, 6, HAL_MAX_DELAY);
if ((readBuffer[0] & 0x80) == 0x00) {
uint32_t data = 0;
data = ((uint32_t) readBuffer[3] >> 4) + ((uint32_t) readBuffer[2] << 4) + ((uint32_t) readBuffer[1] << 12);
*hum = data * 100.0f / (1 << 20);
data = (((uint32_t) readBuffer[3] & 0x0F) << 16) + (((uint32_t) readBuffer[4]) << 8) + (uint32_t) readBuffer[5];
*temp = data * 200.0f / (1 << 20) - 50;
}
}
```
main.c 文件的 USER CODE BEGIN Includes 区域添加头文件:
```c
/* USER CODE BEGIN Includes */
#include "aht20.h"
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */
```
main函数的 USER CODE BEGIN 2 区域添加内容
```c
/* USER CODE BEGIN 2 */
AHT20_Init();
float temp, hum;
char message[50];
/* USER CODE END 2 */
```
完善 main 函数的 while 循环:
```c
while (1) {
AHT20_Read(&temp, &hum);
sprintf(message, "温度:%.1f 湿度:%.1f \n", temp, hum);
HAL_UART_Transmit(&huart2, (uint8_t*) message, strlen(message), 100);
HAL_Delay(2000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
```
## 2.3. 代码分析
### 2.3.1. I2C的地址
在启动传输后,随后传输的I2C首字节包括7位的I2C设备地址 0x38和一个SDA方向位 x(读R:1写W:0)。
![image-20241015145204114](./img/image-20241015145204114.png)
因此,如果是读:实际地址是 0x71如果是写实际地址是0x70注意代码中关于I2C的读写命令
HAL_I2C_Master_Transmit和HAL_I2C_Master_Receive 的地址都使用0x70HAL_I2C_Master_Transmit会自动设置最后一位是0仍然是0x70HAL_I2C_Master_Receive会自动设置最后一位是1就是0x71
### 2.3.2. 温湿度数据的重组
按照AHT20的说明p8返回六个字节的顺序
![image-20241015162601769](./img/image-20241015162601769.png)
![image-20241015162613515](./img/image-20241015162613515.png)
![alt text](img/aht20_data.drawio.png)
## 2.4. I2C的中断和DMA
[中断模式和DMA](https://www.bilibili.com/video/BV1AN41127VL?spm_id_from=333.788.videopod.sections&vd_source=3c8e333d6657680a469ddf0238f01d6a)
# 3. OLED显示
[视频演示](https://www.bilibili.com/video/BV19u4y197df?spm_id_from=333.788.videopod.sections&vd_source=3c8e333d6657680a469ddf0238f01d6a)
开发板上的OLED显示器是单色的显示分辨率为 128 * 64。
![image-20241021082624862](./img/image-20241021082624862.png)
相当于有128 * 64个小灯只需要点亮相应的小灯就可以组成图案。如果使用IO进行操作需要 128 * 64 个IO口8192个显然是不合适的。如何解决
![alt text](img/显示驱动.drawio.png)
其实显示器的矩阵可以看作是一段连续的内存每一个灯就是一个bit单色模式如果是彩色显示器每个灯可能是一个byte或者是多个byte。和内存一样显示器内部也有一个存储区域寄存器和点阵是对应的我们只需要对显示器的控制芯片寄存器的相应位置写入相应的数据显示器对应点就会点亮或者熄灭。这种方式就如同写寄存器一样。
因此显示驱动芯片其实有很多IO与显示器进行连接然后驱动芯片与外部有个通信接口这里是I2C通过接口MCU告诉显示器需要如何显示就好了。这样避免了MCU使用很多IO直接控制显示器。另外这里的通信是I2C但是还有其他的方式例如串口、SPI、VGA、HDMI等。
## 3.1. 显示原理
开发板使用SSD1306作为驱动[显示器资料](../../配套模块资料/OLED显示屏/1.3-ZJY130-2864KSWLG01.pdf)[CH1116芯片资料](<../../配套模块资料/OLED显示屏/CH1116 V0.2.pdf>)
![image-20241021083759719](./img/image-20241021083759719.png)
分成了8个Page大行,每个Page中有8个小行一共就是64行刚好是纵向的分辨率然后每行有128个寄存器表示列这样就得到128*64的一组寄存器。这个和内存中的分页技术是一致的。
### 3.1.1. 通信方式
通信分为两种,指令和数据,指令包含两个字节,数据没有限制。指令表示要从哪个位置开始写数据;数据代表写入什么数据。指令包含页的设置和列的设置两个部分。
每个指令都包含两个字节第一个字节0x00第二个字节表示页/列的设置。指令选择页和选择列但后续只有一个字节了这个后续的一个字节需要携带页还是列具体是哪个页和列因此第二字节可以拆分成为高4位和低4位进行表示其中高4位表示是什么页、列低4位表示具体的参数哪个页、哪个列
#### 3.1.1.1. 指令
![image-20241021090542877](./img/image-20241021090542877.png)
1. 页地址B0~B7其实这里的B就代表设置页地4位的07表示哪个页一共只有8页
2. 列地址把列分为两个字节发送分为高4位和低四位。低位先发高位后发有点像小端模式高4为分别为0和1表示表示列的低4位和高4位因为一共128列因此设置列需要发送两次指令
例如选择页使用Bx的方式高4位B表示设置页的命令低四位X可以是07表示具体是哪一页。这样一个完整的设置页的命令就是0x00,0xb0选择第0页0x00,0xb1选择第1页因为一共只有8页4位的取值范围是015完全覆盖8页的取值范围这样是没有问题的。但是如果选择列列的取值是0127这样使用一条命令是不够的。
在设置列的时候参数字节的高4位是0表示设置列的低四位参数字节的高四位是1表示设置列的高四位00-00设置低四位00-10设置高四位这样列就是00列。注意驱动的第0页其实是从02开始的因此需要发送0x0002,0x0010来选择第0列。
#### 3.1.1.2. 数据
![image-20241021090849503](./img/image-20241021090849503.png)
数据是 0x40 后携带N个字节表示从指令设置的页和列开始连续的设置相应的列。
## 3.2. 显示驱动
在内存中建立一个数组(存储空间)与显存的空间进行对应。当要画图的时候,先在内存中写数据,然后把内存中的数据全部写入到显示器(显存)。这样做有很多好处方便,操作内存比操作显存更方便。但是这样做也有缺陷:
问题:什么缺陷?
OLED_NewFrame 函数用于把内存中一个虚拟屏幕清空这样很快。OLED_ShowFram用于将内存中的一个虚拟屏幕的内容发送到显示器的显存当中这样就可以显示了。
视频演示有个地方错了,关于画点的函数(仔细看看)。
### 3.2.1. 如何显示文字和图片
![image-20241021091927109](./img/image-20241021091927109.png)
显示器和图片都是以点来存放图片或者文字的。如果把上面的文字转换成点阵就可以在点阵的显示器上显示了。如何做到?
任何文字或者图片都可以使用点阵进行描述:
![image-20241021091942446](./img/image-20241021091942446.png)
### 3.2.2. 如何快速的进行显示
1. 抽象基本的功能:画点,划线、画圆。。。
2. 字库通过ASCII码其实是一个整数对应到一个数组点阵然后使用画点的方式画出文字。英文处理相对简单中文处理需要更大的存储空间。
[取模工具](https://led.baud-dance.com/)