MQ-3 酒精传感器是一种基于气敏材料的半导体式气体传感器,内部含有一根加热丝和一个二氧化锡(SnO₂)敏感体,对酒精(乙醇)蒸气具有较高的灵敏度,同时对苯、汽油等干扰气体的灵敏度较低。
当环境中存在酒精蒸气时,敏感体的电阻会随浓度升高而下降。
模块通过板载的 LM393 比较器将这一电阻变化转换为数字开关信号,从 DOUT 引脚输出:无酒精时输出高电平,检测到酒精超标时输出低电平。MQ-3 常用于酒精浓度检测、酒驾检测仪等场合。
1、模块来源
2、参数
工作电压:5V(加热丝需 5V 供电)
输出信号:DOUT 数字开关量(TTL 电平)/ AOUT 模拟量
探测气体:酒精(乙醇)蒸气
管脚数量:4 Pin 
注意
MQ-3 模块的加热丝需要 5V 供电,此时 DOUT 输出的高电平也是 5V。而逻辑派 G1 的 IO 电平为 3.3V,直接连接会损坏 FPGA 引脚。上板前请通过分压电阻或电平转换芯片将 DOUT 电平降至 3.3V。
3、工作原理
原理图
MQ-3 上电后加热丝对敏感体持续加热,敏感体电阻随周围酒精蒸气浓度变化。模块上的 LM393 比较器将敏感体的分压与电位器设定的阈值进行比较,超过阈值时 DOUT 输出低电平,未超过时输出高电平,从而完成"超标/正常"的判断。板载电位器可以调节报警的灵敏度阈值。
模块具有 4 个管脚,分别是 VCC、GND、DOUT、AOUT。
- VCC:电源线,连接 5V
- GND:地线,连接开发板的接地(GND),需与 FPGA 共地
- DOUT:TTL 开关信号输出(正常高电平,超标低电平)
- AOUT:模拟信号输出(浓度越高电压越高,此引脚可以不接)
关于时序
MQ-3 的 DOUT 是一根开关量信号线,并不像 DHT11、DS18B20 那样有数字通信时序,它没有字节、没有帧、没有时钟,只有高低电平两种状态。因此我们的读取节奏完全由 FPGA 自己决定。本例中我们每隔 1 秒采样一次 DOUT 电平,并将当前状态通过串口发送出去。
4、代码设计
整体逻辑分为三步:
- 将异步的 DOUT 信号打两拍同步到系统时钟,消除亚稳态;
- 用一个计数器产生每秒一次的
sec_tick脉冲; - 每个
sec_tick到来时读取当前 DOUT 电平,选择对应的中文字符串,逐字节通过串口发送。
`timescale 1ns / 1ps
module MQ3_top (
input clk ,
input rst_n ,
input mq3_dout ,// MQ-3 DOUT(低电平 = 检测到酒精)
output uart_txd ,
output reg led_alarm // 报警 LED
);
parameter STR_ALARM = 112'hbc_ec_b2_e2_b5_bd_be_c6_be_ab_b3_ac_b1_ea;
// "检测到酒精超标"(7 汉字 = 14 字节)
parameter STR_SAFE = 112'hbe_c6_be_ab_c5_a8_b6_c8_ce_b4_b3_ac_b1_ea;
// "酒精浓度未超标"(7 汉字 = 14 字节)
//信号定义
wire uart_tx_busy ;
wire uart_tx_done ;
reg [ 7: 0] uart_tx_data ;
reg uart_tx_req ;
reg work_en ;
reg [ 6: 0] tx_byte_cnt ;
reg [ 111: 0] str ;
//====================== 同步打拍 ======================
reg D0_syn1;
reg D0_syn2;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
D0_syn1 <= 1'b1;
D0_syn2 <= 1'b1;
end else begin
D0_syn1 <= mq3_dout;
D0_syn2 <= D0_syn1;
end
end
//====================== 1 秒定时器 ======================
localparam ONE_SEC = 50_000_000 - 1; // 50MHz 下 1 秒
reg [25:0] sec_cnt;
reg sec_tick; // 每秒产生一个单周期脉冲
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sec_cnt <= 0;
sec_tick <= 1'b0;
end else if (sec_cnt == ONE_SEC) begin
sec_cnt <= 0;
sec_tick <= 1'b1;
end else begin
sec_cnt <= sec_cnt + 1'b1;
sec_tick <= 1'b0;
end
end
//====================== LED ======================
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
led_alarm <= 1'b0;
else
led_alarm <= ~D0_syn2; // 低电平有效,检测到酒精时 LED 亮
end
//====================== 每秒触发发送 ======================
// 锁存字符串(每秒读一次当前状态)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
str <= 112'b0;
else if (sec_tick && !work_en) begin
if (D0_syn2 == 1'b0)
str <= STR_ALARM; // 当前有酒精
else
str <= STR_SAFE; // 当前正常
end
end
// 发送使能
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
work_en <= 0;
else if (sec_tick && !work_en)
work_en <= 1;
else if (tx_byte_cnt == 7'd15 && uart_tx_done)
work_en <= 0;
else
work_en <= work_en;
end
// byte counter
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx_byte_cnt <= 0;
end else if (work_en) begin
if (tx_byte_cnt == 7'd15 && uart_tx_done)
tx_byte_cnt <= 7'b0;
else if (uart_tx_done)
tx_byte_cnt <= tx_byte_cnt + 1'b1;
end
end
// 串口发送数据控制
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
uart_tx_req <= 1'b0;
uart_tx_data <= 8'b0;
end else if (work_en && !uart_tx_busy) begin
case (tx_byte_cnt) // 根据当前字节计数器发送数据
7'd1: uart_tx_data <= str[111:104]; // "检"/"酒"
7'd2: uart_tx_data <= str[103:96];
7'd3: uart_tx_data <= str[95:88]; // "测"/"精"
7'd4: uart_tx_data <= str[87:80];
7'd5: uart_tx_data <= str[79:72]; // "到"/"浓"
7'd6: uart_tx_data <= str[71:64];
7'd7: uart_tx_data <= str[63:56]; // "酒"/"度"
7'd8: uart_tx_data <= str[55:48];
7'd9: uart_tx_data <= str[47:40]; // "精"/"未"
7'd10: uart_tx_data <= str[39:32];
7'd11: uart_tx_data <= str[31:24]; // "超"/"超"
7'd12: uart_tx_data <= str[23:16];
7'd13: uart_tx_data <= str[15:8]; // "标"/"标"
7'd14: uart_tx_data <= str[7:0];
7'd15: uart_tx_data <= 8'h0A; // 换行符
default: uart_tx_data <= 8'b0;
endcase
uart_tx_req <= 1'b1; // 拉高请求信号
end else begin
uart_tx_req <= 1'b0; // 请求信号拉高仅一个时钟周期
end
end
uart_tx u_uart_tx(
.clk (clk ),
.rst_n (rst_n ),
.uart_tx_req (uart_tx_req & work_en ),// 发送使能请求
.tx_in_data (uart_tx_data ),// UART要发送的数据
.uart_txd (uart_txd ),// UART发送端口
.uart_tx_done (uart_tx_done ),//8bit数据发送完成
.uart_tx_busy (uart_tx_busy ) // 发送忙
);
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
`timescale 1ns / 1ps
//--------------------port----------------------------
module uart_tx (
input clk,
input rst_n,
input uart_tx_req,
input [7:0] tx_in_data,
output reg uart_txd,
output reg uart_tx_done,
output reg uart_tx_busy
);
//----------------------parameter-------------------
//串口有起始和停止位,还有波特率
parameter CLK_FREQ = 26'd50_000_000, // 系统时钟频率 50MHz
UART_BPS = 17'd115200; // UART 波特率 115200
parameter START_BIT = 1'b0; // 起始位
parameter END_BIT = 1'b1; // 停止位
localparam BAUD_CNT_MAX = CLK_FREQ / UART_BPS; //每434个时钟周期发一位数据
reg [12:0]baud_cnt; //434计数器
reg [3:0] bit_cnt; //8位计数器
reg [7:0] data_reg;
//使能信号锁存,发完一个字节(8位)清零
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
uart_tx_busy <= 0;
end
else if (uart_tx_req) begin
uart_tx_busy <= 1'd1;end
else if ((bit_cnt == 4'd09) && (baud_cnt == 'd1)) begin
uart_tx_busy <= 0;
end
else
uart_tx_busy <= uart_tx_busy;
end
//434 counter
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
baud_cnt <= 13'd0;
end
else if (baud_cnt == (BAUD_CNT_MAX-'d1)) begin
baud_cnt <= 0;
end
else if (uart_tx_busy) begin
baud_cnt <= baud_cnt + 'd1;
end
end
//bit_cnt counter
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bit_cnt <= 0;
end
else if(baud_cnt == 13'd1)begin
if ((bit_cnt == 4 'd9) )
bit_cnt <=0;
else if (uart_tx_busy )
bit_cnt <= bit_cnt +1;
end
end
//数据寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_reg<= 0;
end
else if(uart_tx_req)
data_reg <= tx_in_data;
end
//串口发送模块
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
uart_txd <= 1'd1; //空闲时为高电平
end
else if(baud_cnt == BAUD_CNT_MAX - 1)
case (bit_cnt)
4'd0: uart_txd <= START_BIT; // 起始位
4'd1: uart_txd <= data_reg[0]; // 数据最低位
4'd2: uart_txd <= data_reg[1];
4'd3: uart_txd <= data_reg[2];
4'd4: uart_txd <= data_reg[3];
4'd5: uart_txd <= data_reg[4];
4'd6: uart_txd <= data_reg[5];
4'd7: uart_txd <= data_reg[6];
4'd8: uart_txd <= data_reg[7]; // 数据最高位
4'd9: uart_txd <= END_BIT; // 停止位
default: uart_txd <= 1'b1; // 默认状态
endcase
end
// 发送就绪信号逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
uart_tx_done <= 1'b0; // 初始化为未就绪
else if ( baud_cnt == 1'd1 && bit_cnt == 4'd9)
uart_tx_done <= 1'b1; // 发送完成,设置发送就绪信号为有效
else
uart_tx_done <= 1'b0; // 发送中
end
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
代码解释:
- 同步打拍:mq3_dout 是外部异步信号,为防止亚稳态,用两级寄存器(D0_syn1、D0_syn2)同步到系统时钟,之后所有逻辑只使用 D0_syn2。
- 1 秒定时器:50MHz 时钟下,计数器从 0 数到 49999999 刚好是 1 秒,数满时产生一个单周期脉冲 sec_tick 作为发送触发。
- 每秒发送:每个 sec_tick 到来时,若 D0_syn2 为低电平(超标)则锁存报警字符串,为高电平(正常)则锁存安全字符串,随后由字节计数器逐字节送入串口发送模块。2
3
串口发送模块这里我们不再赘述,有需要学习可以去看串口回环那一章。
5、I/O 绑定
引脚图如下
6、程序下载
将程序烧录进开发板,用棉签蘸取少量酒精靠近 MQ-3 传感器,然后我们可以在串口助手上查看具体的信息。无酒精时每秒收到"酒精浓度未超标",检测到酒精超标时每秒收到"检测到酒精超标"。
串口助手设置
波特率 115200、数据位 8、停止位 1、无校验,编码选择 GB2312 / GBK(否则中文会显示为乱码)。