Stm32F103 + NuttX(七):传感器与定时器
发布于 2026-01-26
|
更新于
2026-01-27
|
字数:8707
本文主要是在目前场景下加上传感器模块,以温湿度传感器 DHT11 为例,这个传感器在 NuttX 中也有驱动代码支持,因此直接使用。
但为什么标题还写着定时器呢?因为在用传感器的时候,发现其需要微秒级延时,想了想顺便把定时器这个外设也学习一下,可以说是“为了这碟醋包了这顿饺子”。
本章代码:https://github.com/jklincn/nuttxspace/tree/series/ch07。
传感器
传感器说明
DHT11 是一款湿温度一体化的数字传感器,与单片机之间采用简单的单总线进行通信,仅仅需要一个 I/O 口。
这里的物理连接就不详细说了,如果是正点的精英板,传感器的网格面朝外,有字的一面朝里。在引脚连接上,唯一的数据线接到了 MCU 的 PG11 上面。
数据格式
DHT11 一次完整的数据传输为40bit,高位先出。
数据格式如下:
注意:DHT11 一次通讯时间最大 3ms,主机连续采样间隔建议不小于 100ms。
传输时序
这里就直接贴文档中的图了
DHT11 开始发送数据流程
主机发送开始信号后,延时等待 20us-40us 后读取 DHT11 的回应信号,读取总线为低电平,说明 DHT11 发送响应信号,DHT11 发送响应信号后,再把总线拉高,准备发送数据,每一 bit 数据都以低电平开始,格式见下面图示。如果读取响应信号为高电平,则 DHT11 没有响应,请检查线路是否连接正常。
主机复位信号和DHT11响应信号
数字‘0’信号表示方法
数字‘1’信号表示方法
驱动代码详解
先看板级接口,这里需要实现的是:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
struct dhtxx_config_s
{
// 配置数据引脚方向,mode:false 是输出,true 是输入
CODE void (*config_data_pin)(FAR struct dhtxx_config_s *state, bool mode);
// 设置数据引脚电平,value:false 是低电平,true 是高电平
CODE void (*set_data_pin)(FAR struct dhtxx_config_s *state, bool value);
// 读取数据引脚当前电平,如果是高电平返回 true,低电平返回 false
CODE bool (*read_data_pin)(FAR struct dhtxx_config_s *state);
// 获取单调递增的时间戳
CODE int64_t (*get_clock)(FAR struct dhtxx_config_s *state);
// 具体传感器型号
enum dhtxx_type_e type;
};
|
然后在查看驱动源码时,建议的查看顺序是先看 dhtxx_register,再看 g_dhtxxfops 中的 open/read/write,再往下看。
1
2
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
|
// drivers/sensors/dhtxx.c
#include <nuttx/config.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <debug.h>
#include <nuttx/kmalloc.h>
#include <nuttx/fs/fs.h>
#include <nuttx/signal.h>
#include <nuttx/clock.h>
#include <nuttx/mutex.h>
#include <nuttx/sensors/dhtxx.h>
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
#define DHTXX_START_SIGNAL_LOW_US 20000 // 主机把数据线拉低 20ms,表示开始读取
#define DHTXX_START_SIGNAL_HIGH_US 100 // 主机把数据线拉高 100us,延时等待
#define DHTXX_RESPONSE_SIGNAL_US 85 // 等待应答每段最多 85us
#define DHTXX_TRANSMISSION_START_US 55 // 每位开始前DHT11会拉低电平 50us,这里允许到 55us
#define DHTXX_MIN_ZERO_DURATION 22 // DHT 发送 0 时,高电平持续时间的下限
#define DHTXX_MAX_ZERO_DURATION 30 // DHT 发送 0 时,高电平持续时间的上限
#define DHTXX_MIN_ONE_DURATION 40 // DHT 发送 1 时,高电平持续时间的下限
#define DHTXX_MAX_ONE_DURATION 75 // DHT 发送 1 时,高电平持续时间的上限
#define DHTXX_SAMPLING_PERIOD_S 2 // 采样间隔 2 秒
#define DHTXX_RESPONSE_BITS 40U // 数据长度 40 bit
#define DHT11_MIN_HUM 20.0F // DHT11 湿度测量下限是 20% RH
#define DHT11_MAX_HUM 90.0F // DHT11 湿度测量上限是 90% RH
#define DHT11_MIN_TEMP 0.0F // DHT11 温度测量下限是 0 ℃
#define DHT11_MAX_TEMP 50.0F // DHT11 温度测量下限是 50 ℃
#define DHT12_MIN_HUM 20.0F // DHT12 湿度测量下限是 20% RH
#define DHT12_MAX_HUM 95.0F // DHT12 湿度测量上限是 95% RH
#define DHT12_MIN_TEMP -20.0F // DHT12 温度测量下限是 -20 ℃
#define DHT12_MAX_TEMP 60.0F // DHT12 温度测量下限是 60 ℃
#define DHT22_MIN_HUM 0.0F // DHT22 湿度测量下限是 0% RH
#define DHT22_MAX_HUM 100.0F // DHT22 湿度测量上限是 100%RH
#define DHT22_MIN_TEMP -40.0F // DHT22 温度测量下限是 -40 ℃
#define DHT22_MAX_TEMP 80.0F // DHT22 温度测量下限是 80 ℃
/****************************************************************************
* Private Type
****************************************************************************/
struct dhtxx_dev_s
{
FAR struct dhtxx_config_s *config;
mutex_t devlock;
uint8_t raw_data[5];
};
/****************************************************************************
* Private Function Prototypes
****************************************************************************/
static void dht_standby_mode(FAR struct dhtxx_dev_s *priv);
static void dht_send_start_signal(FAR struct dhtxx_dev_s *priv);
static int dht_prepare_reading(FAR struct dhtxx_dev_s *priv);
static int dht_read_raw_data(FAR struct dhtxx_dev_s *priv);
static bool dht_verify_checksum(FAR struct dhtxx_dev_s *priv);
static bool dht_check_data(FAR struct dhtxx_sensor_data_s *data,
float min_hum, float max_hum,
float min_temp, float max_temp);
static int dht_parse_data(FAR struct dhtxx_dev_s *priv,
FAR struct dhtxx_sensor_data_s *data);
/* Character driver methods */
static int dhtxx_open(FAR struct file *filep);
static ssize_t dhtxx_read(FAR struct file *filep, FAR char *buffer,
size_t buflen);
static ssize_t dhtxx_write(FAR struct file *filep, FAR const char *buffer,
size_t buflen);
/****************************************************************************
* Private Data
****************************************************************************/
static const struct file_operations g_dhtxxfops =
{
dhtxx_open, /* open */
NULL, /* close */
dhtxx_read, /* read */
dhtxx_write, /* write */
};
/****************************************************************************
* Private Functions
****************************************************************************/
// 将传感器设置为待机状态
static void dht_standby_mode(FAR struct dhtxx_dev_s *priv)
{
// 先将引脚配置成输出模式
priv->config->config_data_pin(priv->config, false);
// 输出高电平
priv->config->set_data_pin(priv->config, true);
}
// 向传感器发送采样开始信号
static void dht_send_start_signal(FAR struct dhtxx_dev_s *priv)
{
int64_t start_time;
int64_t current_time;
// 把数据线拉低
priv->config->config_data_pin(priv->config, false);
priv->config->set_data_pin(priv->config, false);
start_time = priv->config->get_clock(priv->config);
// 持续 20 毫秒
while (1)
{
current_time = priv->config->get_clock(priv->config);
if (current_time - start_time >= DHTXX_START_SIGNAL_LOW_US)
{
break;
}
}
// 把数据线拉高
priv->config->set_data_pin(priv->config, true);
start_time = priv->config->get_clock(priv->config);
// 持续 100 微秒
while (1)
{
current_time = priv->config->get_clock(priv->config);
if (current_time - start_time >= DHTXX_START_SIGNAL_HIGH_US)
{
break;
}
}
}
// 检测传感器是否准备好传输数据
static int dht_prepare_reading(FAR struct dhtxx_dev_s *priv)
{
int64_t start_time;
int64_t current_time;
// 把引脚配成输入模式
priv->config->config_data_pin(priv->config, true);
// 传感器应该先拉低电平持续 80 微秒来响应
start_time = priv->config->get_clock(priv->config);
while (!priv->config->read_data_pin(priv->config))
{
current_time = priv->config->get_clock(priv->config);
if (current_time - start_time > DHTXX_RESPONSE_SIGNAL_US)
{
return -1;
}
}
// 再拉高电平持续 80 微秒表示准备输出
while (priv->config->read_data_pin(priv->config))
{
current_time = priv->config->get_clock(priv->config);
if (current_time - start_time > DHTXX_RESPONSE_SIGNAL_US)
{
return -1;
}
}
return 0;
}
// 读取传感器原始 bit 数据
static int dht_read_raw_data(FAR struct dhtxx_dev_s *priv)
{
int64_t start_time;
int64_t end_time;
int64_t current_time;
uint8_t i;
uint8_t j;
j = 0u;
// 总共有 40 位数据
for (i = 0u; i < DHTXX_RESPONSE_BITS; i++)
{
// 每位数据发送前有 50 微秒的低电平
start_time = priv->config->get_clock(priv->config);
while (!priv->config->read_data_pin(priv->config))
{
current_time = priv->config->get_clock(priv->config);
if (current_time - start_time > DHTXX_TRANSMISSION_START_US)
{
return -1;
}
}
// 记录高电平起始时间
start_time = priv->config->get_clock(priv->config);
while (priv->config->read_data_pin(priv->config))
{
current_time = priv->config->get_clock(priv->config);
if (current_time - start_time > DHTXX_MAX_ONE_DURATION)
{
return -1;
}
}
// 记录高电平结束时间
end_time = priv->config->get_clock(priv->config);
// 通过高电平持续时间判断数据是 0 还是 1,这里的阈值是 40 毫秒
if (end_time - start_time >= DHTXX_MIN_ONE_DURATION)
{
priv->raw_data[j] = (priv->raw_data[j] << 1U) | 1U;
}
else
{
priv->raw_data[j] = priv->raw_data[j] << 1U;
}
// 每 8bit 进下一个字节
if (i % 8U == 7U)
{
j++;
}
}
return 0;
}
// 验证数据
static bool dht_verify_checksum(FAR struct dhtxx_dev_s *priv)
{
uint8_t sum;
// 校验方式:前 4 字节相加取低 8 位
sum = (priv->raw_data[0] + priv->raw_data[1] +
priv->raw_data[2] + priv->raw_data[3]) & 0xffu;
return (sum == priv->raw_data[4]);
}
// 检查数据有效性
static bool dht_check_data(FAR struct dhtxx_sensor_data_s *data,
float min_hum, float max_hum,
float min_temp, float max_temp)
{
if (data->hum < min_hum || data->hum > max_hum)
{
return false;
}
if (data->temp < min_temp || data->temp > max_temp)
{
return false;
}
return true;
}
// 根据具体型号解析原始比特数据
static int dht_parse_data(FAR struct dhtxx_dev_s *priv,
FAR struct dhtxx_sensor_data_s *data)
{
int ret = OK;
switch (priv->config->type)
{
case DHTXX_DHT11:
// 取湿度数据
data->hum = priv->raw_data[0];
// 取温度数据
data->temp = priv->raw_data[2];
// 验证数据
if (!dht_check_data(data, DHT11_MIN_HUM, DHT11_MAX_HUM,
DHT11_MIN_TEMP, DHT11_MAX_TEMP))
{
ret = -1;
}
break;
// 以下同理
case DHTXX_DHT12:
data->hum = priv->raw_data[0] + priv->raw_data[1] * 0.1F;
data->temp = priv->raw_data[2] + (priv->raw_data[3] & 0x7fu) * 0.1F;
if (priv->raw_data[3] & 0x80u)
{
data->temp *= -1;
}
if (!dht_check_data(data, DHT12_MIN_HUM, DHT12_MAX_HUM,
DHT12_MIN_TEMP, DHT12_MAX_TEMP))
{
ret = -1;
}
break;
case DHTXX_DHT21:
case DHTXX_DHT22:
case DHTXX_DHT33:
case DHTXX_DHT44:
data->hum = (priv->raw_data[0] << 8u | priv->raw_data[1]) * 0.1F;
data->temp = (((priv->raw_data[2] & 0x7fu) << 8u) |
priv->raw_data[3]) * 0.1F;
if (priv->raw_data[2] & 0x80u)
{
data->temp *= -1;
}
if (!dht_check_data(data, DHT22_MIN_HUM, DHT22_MAX_HUM,
DHT22_MIN_TEMP, DHT22_MAX_TEMP))
{
ret = -1;
}
break;
}
return ret;
}
// 打开设备
static int dhtxx_open(FAR struct file *filep)
{
FAR struct inode *inode = filep->f_inode;
FAR struct dhtxx_dev_s *priv = inode->i_private; // 获取私有设备对象
int ret;
// 串行访问,避免多线程并发导致时序冲突
ret = nxmutex_lock(&priv->devlock);
if (ret < 0)
{
return ret;
}
// 把信号线拉高,让传感器处于待机状态
dht_standby_mode(priv);
// 刚打开设备先等一段时间,避开不稳定期
nxsched_sleep(DHTXX_SAMPLING_PERIOD_S);
// 设备已准备好
nxmutex_unlock(&priv->devlock);
return OK;
}
// 一次完整采样
static ssize_t dhtxx_read(FAR struct file *filep, FAR char *buffer,
size_t buflen)
{
int ret = OK;
FAR struct inode *inode = filep->f_inode;
FAR struct dhtxx_dev_s *priv = inode->i_private;
FAR struct dhtxx_sensor_data_s *data =
(FAR struct dhtxx_sensor_data_s *)buffer;
// buffer 不为空
if (!buffer)
{
snerr("ERROR: Buffer is null.\n");
return -1;
}
// buflen 必须足够放一个结构体
if (buflen < sizeof(FAR struct dhtxx_sensor_data_s))
{
snerr("ERROR: Not enough memory to read data sample.\n");
return -ENOSYS;
}
// 清空 raw_data
memset(priv->raw_data, 0u, sizeof(priv->raw_data));
// 加锁,保证读时序独占
ret = nxmutex_lock(&priv->devlock);
if (ret < 0)
{
return (ssize_t)ret;
}
// 主机发起 start signal
dht_send_start_signal(priv);
// 等待 DHT 应答
if (dht_prepare_reading(priv) != 0)
{
data->status = DHTXX_TIMEOUT;
ret = -1;
goto out;
}
// 读 40bit 原始数据
if (dht_read_raw_data(priv) != 0)
{
data->status = DHTXX_TIMEOUT;
ret = -1;
goto out;
}
// 对数据进行校验
if (!dht_verify_checksum(priv))
{
data->status = DHTXX_CHECKSUM_ERROR;
ret = -1;
goto out;
}
// 解析成温湿度,并做范围校验
if (dht_parse_data(priv, data) != 0)
{
data->status = DHTXX_READ_ERROR;
ret = -1;
}
else
{
data->status = DHTXX_SUCCESS;
}
out:
// 让设备重新处于待机状态
dht_standby_mode(priv);
// 先等待 2 秒再进行解锁,保证读取的时间间隔
nxsched_sleep(DHTXX_SAMPLING_PERIOD_S);
// 已准备好新的读取
nxmutex_unlock(&priv->devlock);
return ret;
}
// 不支持写操作
static ssize_t dhtxx_write(FAR struct file *filep, FAR const char *buffer,
size_t buflen)
{
return -ENOSYS;
}
/****************************************************************************
* Public Functions
****************************************************************************/
int dhtxx_register(FAR const char *devpath,
FAR struct dhtxx_config_s *config)
{
FAR struct dhtxx_dev_s *priv;
int ret;
// 初始化 dhtxx 设备结构体
priv = kmm_malloc(sizeof(struct dhtxx_dev_s));
if (priv == NULL)
{
snerr("ERROR: Failed to allocate instance\n");
return -ENOMEM;
}
// 这里的 config 是板级接口,即驱动代码不知道怎么和开发板上的 DHT11 交互,需要我们来补充。
priv->config = config;
nxmutex_init(&priv->devlock);
// 注册字符设备驱动
ret = register_driver(devpath, &g_dhtxxfops, 0666, priv);
if (ret < 0)
{
nxmutex_destroy(&priv->devlock);
kmm_free(priv);
snerr("ERROR: Failed to register driver: %d\n", ret);
}
return ret;
}
|
定时器
从上面的传输时序图和驱动代码可以看到,DHT11 需要微秒级的时延,而 NuttX 系统最小时间单位默认是 10ms:
~/nuttxspace/$ grep "CONFIG_USEC_PER_TICK" nuttx/build/.config
CONFIG_USEC_PER_TICK=10000
因此这里不能直接使用系统中提供的时间相关的函数,这也是为什么前面的板级接口中有一个 get_clock 函数。
在嵌入式系统中,任何时间的度量都必须追溯到硬件时钟源。时钟源(如晶振)提供稳定的脉冲频率,而硬件计数器通过对这些脉冲进行计数和分频,最终在软件中转化为我们可以识别的时间单位。
时钟源
时钟源有以下 4 类:
- HSE(High-Speed External,高速外部时钟):这是板子上那个 8MHz 的外部晶振。它是最精确、最稳定的时钟源,是产生 72MHz 系统主频的基石。
- HSI(High-Speed Internal,高速内部时钟):MCU 内部自带的 8MHz RC 振荡器。虽然起振快且成本低,但精度受温度影响较大,通常作为 HSE 失效时的备选。
- LSE(Low-Speed External,低速外部时钟):通常外接 32.768kHz 的晶振。它专为 RTC(实时时钟) 设计,即使在主电源断电、依靠纽扣电池供电时也能持续走时。
- LSI(Low-Speed Internal,低速内部时钟):内部低速 RC 振荡器(约 40kHz)。主要用于看门狗(IWDG)或在没有 LSE 的情况下作为 RTC 的廉价替代方案。
我们先看初始化过程中的 stm32_clockconfig 函数,这个函数是根据板级文件 board.h 中的宏定义来建立时钟配置。默认情况下,该函数将复位绝大多数寄存器**,**使能锁相环 (PLL),并根据 NuttX 配置文件中的定义,为所有已开启的外设使能外设时钟。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
void stm32_clockconfig(void)
{
// 确保系统从复位状态开始
// 将 RCC 寄存器恢复到默认值,关闭之前运行的所有 PLL 和时钟源
rcc_reset();
// 如果有必要,复位备份域(RTC相关)
rcc_resetbkp();
#if defined(CONFIG_ARCH_BOARD_STM32_CUSTOM_CLOCKCONFIG)
// 调用板级自定义时钟配置
stm32_board_clockconfig();
#else
// 调用标准的、基于 board.h 定义的固定时钟配置
stm32_stdclockconfig();
#endif
// 使能外设时钟
rcc_enableperipherals();
#ifdef CONFIG_STM32_SYSCFG_IOCOMPENSATION
// 使能 I/O 补偿,目前未启用
stm32_iocompensation();
#endif
}
|
这里我们使用的是标准的时钟配置,即调用 stm32_stdclockconfig 函数,其中是一系列的特定寄存器读写。并且这里要搭配 board.h 一起看,所以我手动搬运了一下其中的内容。(其中的一些 RCC 宏可以在 nuttx/arch/arm/src/stm32/hardware/stm32f10xxx_rcc.h 中找到)
1
2
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
|
// 指明板子上焊接的高速外部晶振(HSE)物理频率为 8MHz
#define STM32_BOARD_XTAL 8000000ul
// 设置 PLL 的时钟来源。这里配置为 RCC_CFGR_PLLSRC(即 HSE)
#define STM32_CFGR_PLLSRC RCC_CFGR_PLLSRC
// HSE 进入 PLL 前的分频器。设置为 0 表示不分频(HSE/1)
#define STM32_CFGR_PLLXTPRE 0
// 锁相环倍频系数。配置为 CLKx9,表示将输入的 8MHz 信号放大 9 倍 。
#define STM32_CFGR_PLLMUL RCC_CFGR_PLLMUL_CLKx9
// 计算出的结果,即 8MHz * 9 = 72MHz
#define STM32_PLL_FREQUENCY (9*STM32_BOARD_XTAL)
// 设置系统时钟切换(Switch)目标。这里配置为 RCC_CFGR_SW_PLL,表示让系统跑在刚才产生的 72MHz PLL 信号上
#define STM32_SYSCLK_SW RCC_CFGR_SW_PLL
// 用于状态检查,确认当前系统时钟源(Switch Status)确实已切换至 PLL 。
#define STM32_SYSCLK_SWS RCC_CFGR_SWS_PLL
// 系统主频,即 72MHz
#define STM32_SYSCLK_FREQUENCY STM32_PLL_FREQUENCY
// AHB 总线 (HCLK):驱动 CPU 和内存。设置为 SYSCLK(不分频),因此 CPU 运行频率也是 72MHz 。
#define STM32_RCC_CFGR_HPRE RCC_CFGR_HPRE_SYSCLK
#define STM32_HCLK_FREQUENCY STM32_PLL_FREQUENCY
// APB2 总线 (PCLK2):驱动高速外设(如 GPIO, TIM1/8)。设置为 HCLK(不分频),所以 APB2 总线频率为 72MHz 。
#define STM32_RCC_CFGR_PPRE2 RCC_CFGR_PPRE2_HCLK
#define STM32_PCLK2_FREQUENCY STM32_HCLK_FREQUENCY
// 专门给 TIM1 和 TIM8 定时器的频率。因为 APB2 不分频,它们直接获得 72MHz 。
#define STM32_APB2_TIM1_CLKIN (STM32_PCLK2_FREQUENCY)
#define STM32_APB2_TIM8_CLKIN (STM32_PCLK2_FREQUENCY)
// APB1 总线 (PCLK1):驱动低速外设(如 TIM2-7, UART3)。STM32F1 规定 APB1 最高只能跑 36MHz,因此这里设置为 HCLKd2(即 2 分频),所以 APB1 总线频率为 36MHz 。
#define STM32_RCC_CFGR_PPRE1 RCC_CFGR_PPRE1_HCLKd2
#define STM32_PCLK1_FREQUENCY (STM32_HCLK_FREQUENCY/2)
// STM32 硬件规定:如果 APB 分频系数大于 1,则供给定时器的时钟是该总线频率的 2 倍 。
#define STM32_APB1_TIM2_CLKIN (2*STM32_PCLK1_FREQUENCY)
#define STM32_APB1_TIM3_CLKIN (2*STM32_PCLK1_FREQUENCY)
#define STM32_APB1_TIM4_CLKIN (2*STM32_PCLK1_FREQUENCY)
#define STM32_APB1_TIM5_CLKIN (2*STM32_PCLK1_FREQUENCY)
#define STM32_APB1_TIM6_CLKIN (2*STM32_PCLK1_FREQUENCY)
#define STM32_APB1_TIM7_CLKIN (2*STM32_PCLK1_FREQUENCY)
static void stm32_stdclockconfig(void)
{
uint32_t regval;
// 检查是否需要 HSE
#if (STM32_CFGR_PLLSRC == RCC_CFGR_PLLSRC) || (STM32_SYSCLK_SW == RCC_CFGR_SW_HSE)
{
volatile int32_t timeout;
// 开启外部高速晶振
regval = getreg32(STM32_RCC_CR);
regval &= ~RCC_CR_HSEBYP; /* Disable HSE clock bypass */
regval |= RCC_CR_HSEON; /* Enable HSE */
putreg32(regval, STM32_RCC_CR);
// 等待就绪
for (timeout = HSERDY_TIMEOUT; timeout > 0; timeout--)
{
/* Check if the HSERDY flag is the set in the CR */
if ((getreg32(STM32_RCC_CR) & RCC_CR_HSERDY) != 0)
{
/* If so, then break-out with timeout > 0 */
break;
}
}
if (timeout == 0)
{
/* In the case of a timeout starting the HSE, we really don't have
* a strategy. This is almost always a hardware failure or
* misconfiguration.
*/
return;
}
}
// 以下是 STM32F100 系列的兼容性处理
# if defined(CONFIG_STM32_VALUELINE) && (STM32_CFGR_PLLSRC == RCC_CFGR_PLLSRC)
# if (STM32_CFGR_PLLXTPRE >> 17) != (STM32_CFGR2_PREDIV1 & 1)
# error STM32_CFGR_PLLXTPRE must match the LSB of STM32_CFGR2_PREDIV1
# endif
/* Set the HSE prescaler */
regval = STM32_CFGR2_PREDIV1;
putreg32(regval, STM32_RCC_CFGR2);
# endif
#endif
/* Value-line devices don't implement flash prefetch/waitstates */
#ifndef CONFIG_STM32_VALUELINE
// 设置 Flash “等待状态” (Latency)
// CPU 跑在 72MHz,但 Flash 存储器的访问速度跟不上这个节奏(STM32F1 的 Flash 在 72MHz 时需要 2 个等待周期)。
regval = getreg32(STM32_FLASH_ACR);
regval &= ~FLASH_ACR_LATENCY_MASK;
regval |= (FLASH_ACR_LATENCY_SETTING | FLASH_ACR_PRTFBE);
putreg32(regval, STM32_FLASH_ACR);
#endif
// 设置总线分频 (AHB/APB)
/* Set the HCLK source/divider */
regval = getreg32(STM32_RCC_CFGR);
regval &= ~RCC_CFGR_HPRE_MASK;
regval |= STM32_RCC_CFGR_HPRE;
putreg32(regval, STM32_RCC_CFGR);
/* Set the PCLK2 divider */
regval = getreg32(STM32_RCC_CFGR);
regval &= ~RCC_CFGR_PPRE2_MASK;
regval |= STM32_RCC_CFGR_PPRE2;
putreg32(regval, STM32_RCC_CFGR);
/* Set the PCLK1 divider */
regval = getreg32(STM32_RCC_CFGR);
regval &= ~RCC_CFGR_PPRE1_MASK;
regval |= STM32_RCC_CFGR_PPRE1;
putreg32(regval, STM32_RCC_CFGR);
/* If we are using the PLL, configure and start it */
#if STM32_SYSCLK_SW == RCC_CFGR_SW_PLL
// PLL 参数设置
regval = getreg32(STM32_RCC_CFGR);
regval &= ~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMUL_MASK);
regval |= (STM32_CFGR_PLLSRC | STM32_CFGR_PLLXTPRE | STM32_CFGR_PLLMUL);
putreg32(regval, STM32_RCC_CFGR);
// 启动 PLL
regval = getreg32(STM32_RCC_CR);
regval |= RCC_CR_PLLON;
putreg32(regval, STM32_RCC_CR);
// 等待 PLL 就绪
while ((getreg32(STM32_RCC_CR) & RCC_CR_PLLRDY) == 0);
#endif
// 切换系统时钟源(即刚刚启动的 PLL)
regval = getreg32(STM32_RCC_CFGR);
regval &= ~RCC_CFGR_SW_MASK;
regval |= STM32_SYSCLK_SW;
putreg32(regval, STM32_RCC_CFGR);
// 确认系统时钟源是刚刚切换过去的那个
while ((getreg32(STM32_RCC_CFGR) & RCC_CFGR_SWS_MASK) != STM32_SYSCLK_SWS);
#if defined(CONFIG_STM32_IWDG) || defined(CONFIG_STM32_RTC_LSICLOCK)
/* Low speed internal clock source LSI */
stm32_rcc_enablelsi();
#endif
}
|
再后面的 rcc_enableperipherals 函数就是根据外设启用情况(通过宏与条件编译)来开启外设的时钟。
计数器
当配置好时钟源之后,硬件计数器就会对这些脉冲进行采样与计数。
计数器有以下 3 类,本文只介绍前两类:
- SysTick:这是 Cortex-M3 内核自带的 24 位递减计数器,专职负责 RTOS 的心跳(Tick),比如在当前配置下就是每 10ms 产生一次中断。主要用于支撑
sleep()、任务切换和毫秒级超时判断 。
- TIM(Timer,定时器):这属于 MCU 外设硬件,可以实现微秒级精确计时、PWM 输出、输入捕获(测频率/脉宽)及触发 ADC 采样等。可以独立于 CPU 运行,可以配置为 1MHz 频率,使每 1 个计数直接代表 1微秒,逻辑直观且极其稳定 。
- DWT(Data Watchpoint and Trace,数据观察点与追踪):这是 Cortex-M 内核中的调试组件,直接记录 CPU 时钟周期(CYCCNT),用于评估代码运行性能和极短时间测量 。在 72MHz 主频下,精度高达约 13.9 纳秒(1/72MHz) 。它不占用中断资源,读取开销极小 。适合在不占用额外定时器(TIM)的情况下,进行微秒级的“忙等”延时或测量极短的代码执行时间 。
Systick
这里我们先看 Systick,它的工作原理是保存了一个“重装载值”,每当时钟源发出一个脉冲时,该值减 1,当减为 0 时,触发一个中断,然后重置重装载值。其初始化代码入口在 nx_start 的 clock_initialize 中。
1
2
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
|
void clock_initialize(void)
{
sched_trace_begin();
#if !defined(CONFIG_SUPPRESS_INTERRUPTS) && \
!defined(CONFIG_SUPPRESS_TIMER_INTS) && \
!defined(CONFIG_SYSTEMTICK_EXTCLK)
/* Initialize the system timer interrupt */
up_timer_initialize();
#endif
#if defined(CONFIG_RTC)
/* Initialize the internal RTC hardware. Initialization of external RTC
* must be deferred until the system has booted.
*/
up_rtc_initialize();
#endif
#if !defined(CONFIG_RTC_EXTERNAL) || \
!defined(CONFIG_RTC)
/* Initialize the time value to match the RTC */
clock_inittime(NULL);
#endif
perf_init();
#ifdef CONFIG_SCHED_CPULOAD_SYSCLK
cpuload_init();
#endif
sched_trace_end();
}
|
其中会调用 up_timer_initialize
1
2
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
|
void up_timer_initialize(void)
{
uint32_t regval;
// 设置中断优先级。通过掩码清除原有优先级位,然后填入 NuttX 默认的优先级值
regval = getreg32(NVIC_SYSH12_15_PRIORITY);
regval &= ~NVIC_SYSH_PRIORITY_PR15_MASK;
regval |= (NVIC_SYSH_PRIORITY_DEFAULT << NVIC_SYSH_PRIORITY_PR15_SHIFT);
putreg32(regval, NVIC_SYSH12_15_PRIORITY);
/* Make sure that the SYSTICK clock source is set correctly */
#if 0 /* Does not work. Comes up with HCLK source and I can't change it */
regval = getreg32(NVIC_SYSTICK_CTRL);
#ifdef CONFIG_STM32_SYSTICK_HCLKd8
regval &= ~NVIC_SYSTICK_CTRL_CLKSOURCE;
#else
regval |= NVIC_SYSTICK_CTRL_CLKSOURCE;
#endif
putreg32(regval, NVIC_SYSTICK_CTRL);
#endif
// 如果使用通用的 ARMV7M SYSTICK 框架并且架构包含定时器,则进入配置。当前未启用。
#if defined(CONFIG_ARMV7M_SYSTICK) && defined(CONFIG_TIMER_ARCH)
up_timer_set_lowerhalf(systick_initialize(true, STM32_HCLK_FREQUENCY, -1));
#else
// 设置重装载值
putreg32(SYSTICK_RELOAD, NVIC_SYSTICK_RELOAD);
// 绑定中断处理函数
irq_attach(STM32_IRQ_SYSTICK, (xcpt_t)stm32_timerisr, NULL);
// 使能硬件计数
putreg32((NVIC_SYSTICK_CTRL_CLKSOURCE | NVIC_SYSTICK_CTRL_TICKINT |
NVIC_SYSTICK_CTRL_ENABLE), NVIC_SYSTICK_CTRL);
// 使能中断线
up_enable_irq(STM32_IRQ_SYSTICK);
#endif
}
|
可以看到这里的重装载值是 SYSTICK_RELOAD
1
2
3
4
|
#define SYSTICK_CLOCK (STM32_HCLK_FREQUENCY)
#define CLK_TCK (1000000/CONFIG_USEC_PER_TICK)
#define SYSTICK_RELOAD ((SYSTICK_CLOCK / CLK_TCK) - 1)
|
SYSTICK_CLOCK 就是我们在 board.h 中定义的频率,即 72 MHz。CONFIG_USEC_PER_TICK 就是 NuttX 编译配置中可以修改的值,这里默认是 10000,那么 CLK_TCK 的值就是 100,代表每秒产生中断的次数。那么这里的重装载值就是 72000000 / 100 - 1 = 719999。这里减 1 的原因是 SysTick 计数器在减到 0 时才会触发中断,并用下一拍的时间重新装载数值。因此,从 719999 减到 0 总共经历了 720000 个时钟周期。在当前 72MHz 的频率下,数 720000 个脉冲所需要的时间正好是 720000 / 72000000 = 0.01 秒 = 10 毫秒。
TIM
Nuttx 中 Timer 的文档:https://nuttx.apache.org/docs/latest/components/drivers/character/timers/index.html
可以看到除了驱动程序外,这里实现了两种定时器抽象,分别是 Oneshot(在指定的延时后触发一次中断) 和 Timer(定期触发任务)。文档还提到,这两种定时器抽象不是必须实现的,如果定时器驱动非常简单,比如只提供周期时钟,那么也可以直接实现 Arch_Timer 这个 OS 接口。
这里选择下来就不考虑实现 NuttX 提供的抽象了,理由如下:
- 因为对于微秒级延迟来说,如果走设备节点调用(比如
open("/dev/timer0", ...)),那么代价有系统调用开销 + 信号量等待 + 上下文切换,这可能会有数个微秒级的开销,极易导致错失电平翻转时间。
- 我们这里使用定时器的目的是编写 DHT11 的板级驱动,即用于驱动的编写,而不是提供给上层用户应用使用。
因此思路就是,借助 NuttX 的 Timer 驱动,然后在板级代码中完成对 Timer 的使用即可,不向上给系统提供抽象。
这里先看通用的驱动文件:nuttx/arch/arm/src/stm32/stm32_tim.c
其中 stm32_tim_init 函数会根据传入的 timer 参数来自动开启 RCC 时钟寄存器,并重置定时器状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
struct stm32_tim_dev_s *stm32_tim_init(int timer)
{
struct stm32_tim_dev_s *dev = NULL;
switch (timer)
{
#ifdef CONFIG_STM32_TIM2
case 2:
dev = (struct stm32_tim_dev_s *)&stm32_tim2_priv;
modifyreg32(STM32_RCC_APB1ENR, 0, RCC_APB1ENR_TIM2EN);
break;
#endif
}
stm32_tim_reset(dev);
return dev;
|
查看 stm32_tim2_priv,这里的 MODE 是指向上计数、向下计数等,初始是未使用状态。
1
2
3
4
5
6
|
struct stm32_tim_priv_s stm32_tim2_priv =
{
.ops = &stm32_tim_ops,
.mode = STM32_TIM_MODE_UNUSED,
.base = STM32_TIM2_BASE,
};
|
查看支持的操作集,这是我们最关心的部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
static const struct stm32_tim_ops_s stm32_tim_ops =
{
.enable = stm32_tim_enable, // 开启计数器
.disable = stm32_tim_disable, // 停止计数器
.setmode = stm32_tim_setmode, // 配置计数模式
.setclock = stm32_tim_setclock, // 设置计数频率
.setperiod = stm32_tim_setperiod, // 设置自动重装载寄存器的值
.getcounter = stm32_tim_getcounter, // 获取当前计数器的数值
.setcounter = stm32_tim_setcounter, // 手动设置当前计数值
.getwidth = stm32_tim_getwidth, // 返回定时器的位宽
.setchannel = stm32_tim_setchannel, // 配置通道模式(如输出 PWM 或禁用)
.setcompare = stm32_tim_setcompare, // 设置比较值(CCR 寄存器),决定 PWM 的占空比。
.getcapture = stm32_tim_getcapture, // 获取输入捕捉到的数值,常用于测量外部信号脉宽。
.setisr = stm32_tim_setisr, // 挂载用户定义的中断处理函数,并处理 NVIC 层的中断使能。
.enableint = stm32_tim_enableint, // 使能定时器内部的中断源
.disableint = stm32_tim_disableint, // 禁用定时器内部的中断源
.ackint = stm32_tim_ackint, // 应答中断标志位
.checkint = stm32_tim_checkint, // 检查特定的中断标志位是否被置位
};
|
我们主要是想做 DHT11 信号电平持续时间检测,更具体的说是在 while(1) 循环中不断读取时间值进行比较,因此只需要使用以下函数即可:
setclock: 将频率设置为 1MHz,即每 1 微秒计数一次
setperiod:因为是 16 位定时器,所以最大值是 0xFFFF,即 65535。设置后计数器每 65.535 毫秒会重置一次,这对于 DHT11 这种基本是微秒级的元器件来说已经够了。
setmode: 设置为向上计数。
enable: 启动计时。
getcounter: 在 while 循环中获取当前“时间戳”。
板级接口实现
本文对 nsh 基础配置做了以下修改:
- 使用新版引脚定义:System Type -> Use the legacy pinmap with GPIO_SPEED_xxx included.(取消选中)
- 将系统日志输出到串口控制台:Device Drivers -> System Logging -> Log to /dev/console(选中)
- 开启文件流:RTOS Features -> Files and I/O -> Enable FILE stream(选中)
修改已同步到代码仓库中,后续都会以这个新的 nsh 作为基础配置,之前的代码还是老配置,所以选对章节代码很重要!
在创建配置前,先创建应用代码目录结构(关键是创建 myapps/dht11/Kconfig 文件让 menuconfig 中生成该应用选项),然后再使用 nsh 配置创建 build 文件夹。
对于 DHT11 实验,打开如下配置:
- 启用传感器驱动支持:Device Drivers -> Sensor Device Support(选中)
- 启用 DHTxx 驱动支持:Device Drivers -> Sensor Device Support -> DHTxx humidity/temperature Sensor support(选中)
- 启用 TIM2:System Type -> STM32 Peripheral Support -> TIM2(选中)
- 启用传感器调试输出:Build Setup -> Debug Options -> Sensor Debug Features(选中)(再选中错误/警告)
- 启用数学库:Library Routines -> Select math library -> Math library from toolchain(选中)
- 启用 float 打印:Library Routines -> Standard C I/O -> Enable floating point in printf(选中)
在 STM32F103ZET6 中,TIM1 和 TIM8 是高级定时器,对于 DHT11 来说用不上,选择 TIM2 这类普通定时器就可以了。
如前所述,我们需要实现 dhtxx_config_s 这个接口结构体,和 GPIO 相关的都比较简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#define GPIO_DHT11_OUTPUT \
(GPIO_OUTPUT | GPIO_CNF_OUTPP | GPIO_MODE_50MHz | GPIO_OUTPUT_SET | GPIO_PORTG | GPIO_PIN11)
#define GPIO_DHT11_INPUT (GPIO_INPUT | GPIO_CNF_INFLOAT | GPIO_MODE_INPUT | GPIO_PORTG | GPIO_PIN11)
static void stm32_dht11_config_data_pin(FAR struct dhtxx_config_s *state, bool mode) {
if (mode) {
stm32_configgpio(GPIO_DHT11_INPUT);
} else {
stm32_configgpio(GPIO_DHT11_OUTPUT);
}
}
static void stm32_dht11_set_data_pin(FAR struct dhtxx_config_s *state, bool value) {
stm32_gpiowrite(GPIO_DHT11_OUTPUT, value);
}
static bool stm32_dht11_read_data_pin(FAR struct dhtxx_config_s *state) {
return stm32_gpioread(GPIO_DHT11_INPUT);
}
|
初始化函数主要是调用 stm32_tim_init 和 dhtxx_register:
1
2
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
|
int stm32_dht11_init(void) {
FAR struct stm32_dht11_config_s *priv = &g_dht11_priv;
int ret;
// Initialize TIM2 instance
priv->tim = stm32_tim_init(2);
if (!priv->tim) {
snerr("ERROR: Failed to initialize TIM2\n");
return -ENODEV;
}
// Configure TIM2
STM32_TIM_SETCLOCK(priv->tim, 1000000);
STM32_TIM_SETPERIOD(priv->tim, 0xFFFF);
STM32_TIM_SETMODE(priv->tim, STM32_TIM_MODE_UP);
STM32_TIM_ENABLE(priv->tim);
// Link interface operations to our static functions
priv->dev.config_data_pin = stm32_dht11_config_data_pin;
priv->dev.set_data_pin = stm32_dht11_set_data_pin;
priv->dev.read_data_pin = stm32_dht11_read_data_pin;
priv->dev.get_clock = stm32_dht11_get_clock;
priv->dev.type = DHTXX_DHT11;
// Register the character driver
ret = dhtxx_register(CONFIG_STM32_DHT11_DEVPATH, &priv->dev);
if (ret < 0) {
snerr("ERROR: Failed to register DHT11: %d\n", ret);
/* If registration fails, release the timer to save power */
stm32_tim_deinit(priv->tim);
return ret;
}
return OK;
}
|
复杂的是 get_clock,因为这是一个 16 位定时器,但 get_clock 接口的返回值定义的是 int64_t,因此要进行软件模拟:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
static int64_t stm32_dht11_get_clock(FAR struct dhtxx_config_s *state) {
static uint32_t rollover_count = 0; // 记录定时器“跑了几圈”
static uint16_t last_cnt = 0; // 记录“上一次”看到的时间值
FAR struct stm32_dht11_config_s *priv = (FAR struct stm32_dht11_config_s *)state;
uint16_t current_cnt = (uint16_t)STM32_TIM_GETCOUNTER(priv->tim);
if (current_cnt < last_cnt) {
rollover_count++;
}
last_cnt = current_cnt;
return ((int64_t)rollover_count << 16) | current_cnt;
}
|
- 读取 TIM2 硬件寄存器 CNT 的值
- 因为定时器配置为向上计数 (
STM32_TIM_MODE_UP),正常情况下 current 应该永远比 last 大。如果现在的读数(比如 10)竟然比上一次的读数(比如 65530)还要小,说明定时器刚刚跨越了终点(65535)又从 0 开始了。检测到这种情况,立刻把“圈数” (rollover_count) 加 1。
- 把当前的读数存入
last_cnt,作为下一次判断的基准。
- 拼接 64 位时间戳:高位(圈数):
rollover_count << 16。把圈数左移 16 位,相当于把它放到了 64 位整数的“高位”部分。低位(当前值):| current_cnt。把当前的 16 位计数值填入低 16 位。
应用代码编写
这里的代码就是打开设备文件,然后进行 read 读取。
按照 POSIX 标准,read 的返回值应该是读取到的字节数,但 dhtxx.c 中没有实现这个。打算提个 PR 完善一下。
最新:PR 已合并
1
2
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
|
#include <fcntl.h>
#include <nuttx/config.h>
#include <nuttx/sensors/dhtxx.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int fd;
struct dhtxx_sensor_data_s data;
ssize_t ret;
fd = open("/dev/dht0", O_RDONLY);
if (fd < 0) {
printf("Failed to open /dev/dht0\n");
return -1;
}
printf("Reading DHT11...\n");
// Read data from DHT11 sensor
ret = read(fd, &data, sizeof(data));
if (ret < 0) {
printf("Read failed. ret=%d\n", ret);
} else {
if (data.status == DHTXX_SUCCESS) {
printf("Humidity: %.1f %%\n", data.hum);
printf("Temperature: %.1f C\n", data.temp);
} else {
printf("Error: %d\n", data.status);
}
}
close(fd);
return 0;
}
|
最后下板后可以正常获取传感器输出
nsh> dht11
Reading DHT11...
Humidity: 22.0 %
Temperature: 21.0 C