9.1 项目概述
本章设计一个环境监测传感器从设备,具有以下硬件资源:
-
2个有源蜂鸣器(BEEP1、BEEP2):高电平发声。
-
3个LED(LED1~LED3):低电平点亮。
-
1路光敏电阻(OPTO_ADC):通过ADC采集电压,电压值与光强成反比。
-
1路可调电阻(RES_ADC):通过ADC采集电压,电压值与电阻值成反比。
设备通过RS485接口以Modbus RTU协议与主站通信,从站地址设为02H。
9.2 硬件引脚定义
根据提供的表格,硬件连接如下:
| 功能 | 引脚 | 电平逻辑 | Modbus类型 |
|---|---|---|---|
| BEEP1(蜂鸣器1) | PB15 | 高电平发声 | DO |
| BEEP2(蜂鸣器2) | PB14 | 高电平发声 | DO |
| LED1 | PB11 | 低电平发光 | DO |
| LED2 | PB12 | 低电平发光 | DO |
| LED3 | PB13 | 低电平发光 | DO |
| OPTO_ADC(光敏) | PA1 | ADC输入(电压与光强成反比) | AI(输入寄存器) |
| RES_ADC(可调电阻) | PA2 | ADC输入(电压与电阻成反比) | AI(输入寄存器) |
电平逻辑总结:
-
蜂鸣器:高电平驱动(1→响,0→静音)
-
LED:低电平驱动(1→亮,0→灭?注意表格中LED是"低电平发光",所以当输出低电平时LED亮。但寄存器描述中"1-亮"对应我们需要输出低电平,所以代码中需要取反)
9.3 Modbus寄存器地址分配
根据需求,设备地址为02H,寄存器分配如下:
| 设备地址 | 寄存器地址 | 寄存器类别 | 用途 | 描述(位值) |
|---|---|---|---|---|
| 02H | 0000H | DO | 控制蜂鸣器1 | 1-响 |
| 0001H | DO | 控制蜂鸣器2 | 1-响 | |
| 0002H | DO | 控制LED1 | 1-亮 | |
| 0003H | DO | 控制LED2 | 1-亮 | |
| 0004H | DO | 控制LED3 | 1-亮 | |
| 02H | 0000H | AI | 读取光敏电压 | 12位ADC值,0~4095对应0~3.3V |
| 0001H | AI | 读取可调电阻电压 | 12位ADC值,0~4095对应0~3.3V |
注意:
-
线圈(DO)使用地址0x0000~0x0004,共5个。
-
输入寄存器(AI)使用地址0x0000~0x0001,共2个。输入寄存器是16位的,存放ADC转换结果(12位有效,高4位补0)。
9.4 代码整体结构分析
代码基于STM32 HAL库和FreeRTOS,与上一章类似,但使用了ADC模块。
9.4.1 头文件与宏定义
cs
#ifdef USE_ENV_MONITOR_SENSOR
extern ADC_HandleTypeDef hadc; // 引用HAL库的ADC句柄
#define SLAVE_ADDR 2
#define NB_BITS 5 // 线圈数量(蜂鸣器2 + LED3 = 5)
#define NB_INPUT_BITS 0 // 离散输入数量(无)
#define NB_REGISTERS 0 // 保持寄存器数量(无)
#define NB_INPUT_REGISTERS 2 // 输入寄存器数量(光敏+可调电阻)
#endif
解释:
-
NB_BITS = 5:对应地址0x0000~0x0004,5个线圈输出。 -
NB_INPUT_REGISTERS = 2:对应地址0x0000~0x0001,2个输入寄存器。 -
extern ADC_HandleTypeDef hadc:ADC句柄,需要在其他地方定义并初始化(通常在HAL生成的代码中)。
9.4.2 任务函数中的ADC读取部分
在收到请求后,更新输入寄存器(读取ADC)的代码:
cs
#ifdef USE_ENV_MONITOR_SENSOR
/* READ ADC to update tab_input_registers */
for (int i = 0; i < 2; i++)
{
HAL_ADC_Start(&hadc); // 启动ADC转换
if (HAL_OK == HAL_ADC_PollForConversion(&hadc, 100)) // 等待转换完成,超时100ms
{
mb_mapping->tab_input_registers[i] = HAL_ADC_GetValue(&hadc); // 读取转换结果
}
}
#endif
详细解释:
-
HAL_ADC_Start(&hadc):启动ADC转换(单次模式)。 -
HAL_ADC_PollForConversion(&hadc, 100):轮询等待转换完成,超时100ms。如果返回HAL_OK,表示转换成功。 -
HAL_ADC_GetValue(&hadc):获取转换结果,通常是12位值(0~4095)。 -
将结果存入
mb_mapping->tab_input_registers[i],i=0对应光敏,i=1对应可调电阻。
注意:这里每次收到请求后都会重新启动ADC转换,这意味着主站每次读取输入寄存器都会触发一次新的ADC采样。如果ADC采样需要稳定时间,可能需要在循环外定期更新,但这里简单处理为每次请求时更新。
9.4.3 输出控制部分(根据线圈状态更新硬件)
在回复请求后,根据tab_bits的值控制蜂鸣器和LED:
cs
#ifdef USE_ENV_MONITOR_SENSOR
/* beep1 */
if (mb_mapping->tab_bits[0])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_SET); // beep1 高电平响
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_RESET);
/* beep2 */
if (mb_mapping->tab_bits[1])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
/* LED1 */
if (mb_mapping->tab_bits[2])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET); // LED1 低电平亮
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET);
/* LED2 */
if (mb_mapping->tab_bits[3])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET);
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
/* LED3 */
if (mb_mapping->tab_bits[4])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET);
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET);
#endif
电平转换说明:
-
蜂鸣器:
tab_bits为1时,输出高电平(GPIO_PIN_SET);为0时输出低电平(GPIO_PIN_RESET)。符合"1-响"的定义。 -
LED:
tab_bits为1时,输出低电平(GPIO_PIN_RESET);为0时输出高电平(GPIO_PIN_SET)。这是因为硬件是低电平点亮,所以逻辑1对应亮,硬件上需要输出低电平。
9.4.4 完整的任务循环结构
任务函数整体框架与第八章类似,但这里使用了输入寄存器,且无离散输入。关键部分如下:
cs
void StartDefaultTask(void *argument)
{
// 初始化...
ctx = modbus_new_rtu("uart1", 115200, 'N', 8, 1);
modbus_set_slave(ctx, SLAVE_ADDR);
query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);
// 创建映射:线圈起始0数量5,离散输入0数量0,输入寄存器0数量2,保持寄存器0数量0
mb_mapping = modbus_mapping_new_start_address(0, NB_BITS,
0, NB_INPUT_BITS,
0, NB_INPUT_REGISTERS,
0, NB_REGISTERS);
rc = modbus_connect(ctx);
if (rc == -1) {
// 错误处理
}
for (;;) {
do {
rc = modbus_receive(ctx, query);
} while (rc == 0);
if (rc == -1 && errno != EMBBADCRC) {
continue;
}
// 更新输入寄存器(ADC采样)
for (int i = 0; i < NB_INPUT_REGISTERS; i++) {
HAL_ADC_Start(&hadc);
if (HAL_ADC_PollForConversion(&hadc, 100) == HAL_OK) {
mb_mapping->tab_input_registers[i] = HAL_ADC_GetValue(&hadc);
}
}
// 回复请求
rc = modbus_reply(ctx, query, rc, mb_mapping);
if (rc == -1) {
// 错误处理
}
// 根据线圈状态更新硬件输出
// ...(上面已列出)
}
// 清理资源
modbus_mapping_free(mb_mapping);
vPortFree(query);
modbus_close(ctx);
modbus_free(ctx);
vTaskDelete(NULL);
}
9.5 关键细节深入解析
9.5.1 ADC读取的注意事项
-
ADC初始化 :需要确保ADC已经在
MX_ADC_Init()中正确配置(时钟、通道、分辨率、对齐方式等)。代码中hadc是HAL库生成的句柄,需包含对应头文件。 -
转换模式:使用单次转换模式,每次启动后轮询等待完成。如果ADC采样时间较长,轮询超时时间要足够(这里100ms可能过大,但安全)。
-
通道切换 :这里只有一个ADC句柄,假设已经配置为扫描模式或通过软件依次选择通道?代码中
for循环两次,每次都启动同一个hadc,但并没有切换通道。这通常是不正确的,因为hadc可能只配置了一个通道。正确的做法是需要配置ADC为多通道规则组,或者使用两个不同的ADC句柄,或者在启动前设置通道。假设实际硬件有两个独立的ADC通道(如PA1和PA2),它们可能属于同一个ADC模块的不同通道。正确代码应该:
-
方法1:使用ADC的扫描模式,一次启动连续转换两个通道,然后依次读取结果。
-
方法2:在每次启动前设置转换通道(通过
HAL_ADC_ConfigChannel)。
但当前代码简单地在循环中启动同一个ADC,可能导致两次读取的都是同一个通道(最后一次配置的通道)。这需要根据实际硬件配置来判断。如果两个通道是独立的ADC实例(例如ADC1和ADC2),则应该有两个句柄。但从代码看只有一个
hadc,可能实际应用中已经将两个通道配置为扫描模式,每次启动后读取两个结果,但这里却分别启动两次,会覆盖结果。所以这是一个潜在的bug或简化。在讲解时,可以指出这个问题,并说明正确做法。
-
-
结果存储 :ADC值是12位,存入16位的
tab_input_registers是合适的。主站读取时获得原始ADC值,需要自己转换成物理量(如电压)。
9.5.2 输出控制的电平极性
-
蜂鸣器:高电平有效,所以
tab_bits直接对应引脚电平。 -
LED:低电平有效,所以
tab_bits为1时引脚应置低,为0时置高。代码中正确实现了取反逻辑。
9.5.3 地址映射关系
-
线圈地址0x0000对应
tab_bits[0](蜂鸣器1) -
线圈地址0x0001对应
tab_bits[1](蜂鸣器2) -
线圈地址0x0002对应
tab_bits[2](LED1) -
线圈地址0x0003对应
tab_bits[3](LED2) -
线圈地址0x0004对应
tab_bits[4](LED3) -
输入寄存器地址0x0000对应
tab_input_registers[0](光敏ADC) -
输入寄存器地址0x0001对应
tab_input_registers[1](可调电阻ADC)
9.5.4 更新时机
-
输入寄存器更新:每次收到有效请求后立即更新,保证主站读到最新采样值。
-
输出更新:在回复后立即更新,使主站的写操作立即生效。
9.6 完整点表总结
| 类型 | Modbus地址 | PLC地址(常见) | 名称 | 访问 | 说明 |
|---|---|---|---|---|---|
| 线圈 | 0x0000 | 00001 | 蜂鸣器1 | 读写 | 1=响 |
| 线圈 | 0x0001 | 00002 | 蜂鸣器2 | 读写 | 1=响 |
| 线圈 | 0x0002 | 00003 | LED1 | 读写 | 1=亮 |
| 线圈 | 0x0003 | 00004 | LED2 | 读写 | 1=亮 |
| 线圈 | 0x0004 | 00005 | LED3 | 读写 | 1=亮 |
| 输入寄存器 | 0x0000 | 30001 | 光敏电压ADC值 | 只读 | 0~4095对应0~3.3V |
| 输入寄存器 | 0x0001 | 30002 | 可调电阻ADC值 | 只读 | 0~4095对应0~3.3V |
注意:PLC地址通常从1开始,且类型前缀不同(0x对应0类,3x对应3类),这里只是示意。
9.7 调试与验证
-
读输入寄存器(功能码04):读取地址0~1,观察ADC值是否随光照和电位器变化。
-
写线圈(功能码05/15):控制蜂鸣器和LED,验证动作是否正确。
-
注意电平极性:LED亮时测量引脚应为低电平,蜂鸣器响时引脚应为高电平。
-
ADC精度:12位ADC,参考电压3.3V,可通过电压表验证转换关系。
9.8 可能的改进点
-
ADC多通道正确配置:如果两个通道在同一ADC,应使用扫描模式,一次启动后读取多个结果,避免重复启动。
-
错误处理:如果ADC转换超时,应保留旧值或返回异常。
-
输入寄存器更新频率:可以在任务空闲时定时更新,而不是每次请求都重新采样,避免频繁启动ADC影响实时性。
9.9 本章核心知识点总结
-
输入寄存器(AI)的使用:用于传输16位的模拟量数据,如ADC采样值。
-
ADC读取流程:启动转换→等待完成→读取结果→存入映射。
-
电平极性转换:根据硬件电路正确映射逻辑值到物理电平(特别注意LED低电平点亮的情况)。
-
多通道ADC处理:需要注意切换通道或使用扫描模式。
-
Modbus寄存器地址分配:线圈和输入寄存器地址空间独立,通过不同功能码访问。
-
从设备程序结构:初始化→创建映射→连接→循环接收请求→更新输入→回复→更新输出。