2、硬件接口基础:模拟信号与数字信号、I2C协议、UART协议
做pH传感器驱动,说白了就是跟硬件打交道。你得先搞清楚传感器怎么跟你「说话」,你才能听懂它说了什么。这一章,我带你过一遍最核心的三个概念:模拟信号与数字信号的区别、I2C协议、UART协议。这些都是我这些年做项目天天用的东西,你吃透了,后面写驱动就顺了。
2.1 模拟信号 vs 数字信号:传感器的「语言」
pH传感器输出的原始信号,其实是个模拟电压。什么意思呢?就是它根据水里的氢离子浓度,输出一个连续变化的电压值。比如pH=7时输出0V,pH=4时输出0.3V,pH=10时输出-0.3V。这个电压是连续的,理论上可以取任意值。
但我们的单片机不认识模拟电压。它只认识0和1。所以中间需要个「翻译官」——ADC(模数转换器)。
关键点:模拟信号是连续的,数字信号是离散的。ADC的位数决定了你能把模拟信号「切」得多细。比如12位ADC,能把0-3.3V分成4096份。分辨率就是3.3V/4096 ≈ 0.8mV。
我在项目中遇到过一个问题:有个学生用10位ADC去读pH传感器,结果发现数据跳得厉害。为什么?因为10位ADC只有1024个台阶,每个台阶对应约3.2mV。而pH传感器每变化0.1pH,电压才变化约59mV(25°C时)。算下来,10位ADC只能分辨大约0.05pH的变化。但实际噪声一叠加,读数就飘了。后来换成12位ADC,立马稳了。
我的建议:做pH传感器,至少用12位ADC。如果条件允许,用16位或者带PGA(可编程增益放大器)的ADC,效果更好。
2.2 I2C协议:两根线搞定一切
现在很多pH传感器模块,内部已经集成了ADC和信号调理电路,直接通过I2C接口输出数字信号。比如我常用的Atlas Scientific的pH传感器,就是I2C接口。
I2C协议,说白了就是两根线:SCL(时钟线)和SDA(数据线)。所有设备都挂在这两根线上,通过地址来区分。主设备(比如STM32)发起通信,从设备(比如pH传感器)响应。
I2C的通信流程,我总结成三步:
- 起始条件:SCL高电平时,SDA从高变低。
- 数据传输:每8位数据后跟一个应答位(ACK)。主设备发送地址+读写位,从设备应答后开始传数据。
- 停止条件:SCL高电平时,SDA从低变高。
嗯,这里要注意:I2C的地址是7位的,但实际发送时是8位(左移1位,最低位是读写位)。比如地址0x63,发送时是0xC6(写)或0xC7(读)。
我曾经踩过一个坑:有个传感器模块的地址是0x63,但我用0x63去读,死活没反应。查了半天手册才发现,人家手册里写的地址是7位值,但发送时要左移一位。你想想看,这种细节不注意,能卡你半天。
避坑指南:I2C总线上需要上拉电阻,一般是4.7kΩ。如果距离长或者设备多,可以换成2.2kΩ。我曾经因为没加上拉电阻,总线一直低电平,折腾了一下午才发现。
下面是一个典型的I2C读pH传感器代码片段:
// 读取pH传感器数据(I2C接口)
uint8_t i2c_read_ph_sensor(uint8_t slave_addr, uint8_t reg_addr, uint8_t *data, uint8_t len) {
// 1. 发送起始条件 + 从设备地址(写)
i2c_start();
i2c_send_byte(slave_addr << 1 | 0); // 写操作
if (!i2c_wait_ack()) return 1; // 等待应答
// 2. 发送寄存器地址
i2c_send_byte(reg_addr);
if (!i2c_wait_ack()) return 1;
// 3. 重新发送起始条件 + 从设备地址(读)
i2c_start();
i2c_send_byte(slave_addr << 1 | 1); // 读操作
if (!i2c_wait_ack()) return 1;
// 4. 读取数据
for (uint8_t i = 0; i < len; i++) {
data[i] = i2c_read_byte();
if (i < len - 1) {
i2c_send_ack(); // 发送应答
} else {
i2c_send_nack(); // 最后一个字节发送非应答
}
}
// 5. 发送停止条件
i2c_stop();
return 0;
}
小技巧:调试I2C时,用逻辑分析仪看波形最直观。我习惯把SCL和SDA都接到逻辑分析仪上,一看起始条件、地址、数据、应答,所有问题一目了然。
2.3 UART协议:老牌通信方式
UART(通用异步收发器)是另一种常见的接口。有些pH传感器模块,比如DFRobot的模拟pH传感器,输出的是模拟信号。但也有一些数字pH传感器,直接通过UART输出数据。
UART的特点:
- 异步:没有时钟线,靠波特率同步。双方得约定好速度,比如9600、115200。
- 全双工:可以同时发送和接收,用TX和RX两根线。
- 数据格式:起始位(1位)+ 数据位(5-9位,常用8位)+ 校验位(可选)+ 停止位(1-2位)。
我个人习惯用115200波特率,8位数据,无校验,1位停止位。也就是常说的「115200 8N1」。
UART通信的坑,我遇到过不少。最典型的是波特率不匹配。有一次我调试一个pH传感器模块,发命令过去没反应。用示波器一看,TX引脚有波形,但频率不对。原来传感器默认是9600波特率,我代码里配的是115200。你想想看,这能通才怪。
避坑指南:拿到一个新模块,第一件事就是看手册里的默认波特率。很多模块出厂是9600,但有些是115200。如果通信不上,先检查波特率。
下面是一个UART读取pH传感器数据的例子:
// 通过UART发送命令并读取pH值
void uart_read_ph_sensor(UART_HandleTypeDef *huart) {
uint8_t cmd[] = "R\r\n"; // 读取命令,具体看传感器手册
uint8_t buf[16] = {0};
// 发送命令
HAL_UART_Transmit(huart, cmd, sizeof(cmd), 100);
// 等待并接收响应
HAL_Delay(100); // 给传感器一点处理时间
HAL_UART_Receive(huart, buf, 10, 100);
// 解析数据(假设返回格式是 "pH:7.25\r\n")
float ph_value = 0.0;
if (sscanf((char *)buf, "pH:%f", &ph_value) == 1) {
printf("pH value: %.2f\n", ph_value);
} else {
printf("Parse error: %s\n", buf);
}
}
我的经验:UART调试时,先用串口助手(比如SSCOM、Putty)手动发命令,看返回数据对不对。确认没问题了,再写代码。这样能快速定位问题是出在硬件还是软件。
2.4 如何选择:I2C还是UART?
这个问题,我经常被问到。我的建议很简单:
| 场景 | 推荐接口 | 原因 |
|---|---|---|
| 多传感器挂在一根总线上 | I2C | 地址区分,节省引脚 |
| 距离较远(>1米) | UART(RS232/RS485) | 抗干扰能力强 |
| 传感器自带ADC和数字输出 | I2C或UART都行 | 看传感器手册支持哪个 |
| 需要高速传输 | I2C(标准模式100kHz,快速模式400kHz) | UART速度受限于波特率 |
嗯,其实没有绝对的好坏。我做过一个项目,用了4个I2C pH传感器,每个地址不同,一根总线上搞定。也做过一个项目,传感器离主板3米远,只能用UART转RS485来传。
好了,这一章就到这里。模拟信号和数字信号的区别,I2C和UART的基本用法,你都清楚了。下一章,我们开始真正动手写pH传感器的驱动代码。
本章要点回顾:
- pH传感器输出模拟电压,需要ADC转换成数字信号
- I2C用两根线(SCL、SDA),通过地址区分设备,适合短距离多设备
- UART用两根线(TX、RX),异步通信,适合长距离
- 调试时多用逻辑分析仪和串口助手,能省不少时间