本篇博文写的是一个最近做的小项目。具体的功能为:检测温湿度传感器(dht11)的数据并显示到oled上面,通过mqtt协议传输到onenet平台。本项目功能简单,只是将之前博文介绍的一些东西整合起来。
具体使用的技术有:linux单总线驱动dht11、i2c框架控制oled、poll机制(驱动和应用)、内核定时器的使用、队列、mqtt协议。
使用到的环境:imx6ull开发板(正点原子阿波罗开发板)、ubuntu18(已安装交叉环境)、dht11温湿度传感器、i2c接口的0.96寸oled、onenet平台。
dht11是使用单总线协议控制的数字温湿度传感器。这里使用模拟IO的方式进行驱动dht11。
主机IO引脚拉低数据线,保持 t1(至少 18ms)时间,拉高数据线 t2(20~ 40us)时间,等待 DHT11 的响应。dht11 会拉低数据线并保持 t3(40~50us)时间,作为响应信号, dht11 拉高数据线,保持 t4(40 ~50us)时间后,开始输出数据。
dht11的时序比较简单。程序控制按照其时序编写即可。Linux驱动中使用udelay、mdelay作为微秒级、毫秒级延时。程序代码如下。
a)dht11驱动的结构体,后面的程序需要使用这个结构体创建设备、控制IO,这里记录下dht11使用的系统资源。
struct dht11_device{
dev_t dev_id; //设备号
int major; //主设备号
int minor; //次设备号
struct cdev cdev; //字符设备
struct class *class; //类
struct device *device; //设备
struct device_node *device_node; //设备节点
int gpio; //数据引脚
uint16_t humidity, temperature; //检测到的温湿度数据
int data_flag; //数据更新标志位
struct timer_list timer;
};
struct dht11_device g_dht11_device;
b) dht11本身的单总线驱动程序
#define DHT11_DelayMs(t) mdelay(t)
#define DHT11_DelayUs(t) udelay(t)
#define DHT11_PIN_HIGH 1
#define DHT11_PIN_LOW 0
#define DHT11_IO_OUT(dht11) gpio_direction_output(dht11->gpio, 1);
#define DHT11_IO_IN(dht11) gpio_direction_input(dht11->gpio)
#define DHT11_WRITE(dht11,bit) gpio_set_value(dht11->gpio, bit)
#define DHT11_READ(dht11) gpio_get_value(dht11->gpio)
//设置引脚电平
void dht11_set_gpio(struct dht11_device* dht11,int value)
{
if(value == DHT11_PIN_HIGH){
gpio_direction_output(g_dht11_device.gpio, DHT11_PIN_HIGH);//gpio_direction_output(dht11->gpio, DHT11_PIN_HIGH);
}else if(value == DHT11_PIN_LOW){
gpio_direction_output(g_dht11_device.gpio, DHT11_PIN_LOW);//gpio_direction_output(dht11->gpio, DHT11_PIN_LOW);
}
}
//等待响应
static int dht11_wait_for_ready(struct dht11_device *dht)
{
int timeout;
timeout = 400;
while (DHT11_READ(dht) && timeout) // 等待低电平到来
{
udelay(1);
--timeout;
}
if (!timeout)
{
printk("timeout %d\n", __LINE__);
return -1; // 超时
}
timeout = 1000;
while (!DHT11_READ(dht) && timeout) // 等待高电平到来
{
udelay(1);
--timeout;
}
if (!timeout)
{
printk("timeout %d\n", __LINE__);
return -1; // 超时
}
timeout = 1000;
while (DHT11_READ(dht) && timeout) // 等待高电平结束
{
udelay(1);
--timeout;
}
if (!timeout)
{
printk("timeout %d\n", __LINE__);
return -1; // 超时
}
return 0;
}
//开始信号
static int dht11_start(struct dht11_device *dht)
{
DHT11_IO_OUT(dht);
DHT11_WRITE(dht,0);
mdelay(20);
DHT11_WRITE(dht,1);
udelay(30);
DHT11_IO_IN(dht); // 设置为输入
udelay(2);
if (dht11_wait_for_ready(dht)) return -1;
return 0;
}
//读取数据
static int dht11_read_byte(struct dht11_device *dht,unsigned char *byte)
{
unsigned char i;
unsigned char bit = 0;
unsigned char data = 0;
int timeout = 0;
for (i = 0; i < 8; i++)
{
timeout = 1000;
while (DHT11_READ(dht) && timeout) // 等待变为低电平
{
udelay(1);
--timeout;
}
if (!timeout)
{
printk("timeout %d\n", __LINE__);
return -1; // 超时
}
timeout = 1000;
while (!DHT11_READ(dht) && timeout) // 等待变为高电平
{
udelay(1);
--timeout;
}
if (!timeout)
{
printk("timeout %d\n", __LINE__);
return -1; // 超时
}
udelay(40);
bit = DHT11_READ(dht);
data <<= 1;
if (bit)
{
data |= 0x01;
}
// data <<= 1; // 导致错误的原因 : 移位要放前面,不能放在这里,若放在后面一旦获取最后一个位就会多移动一位导致数据不对
}
*byte = data;
return 0;
}
//从DHT11读取一次数据
//temp:温度值(范围:0~50°)
//humi:湿度值(范围:20%~90%)
//返回值:0,正常;1,读取失败
static int dht11_read_data(struct dht11_device* dht)
{
unsigned char data[5] = {0};
int i = 0,ret = 0;
// 启动信号
if (dht11_start(dht) != 0)
{
printk("dht11 start failed\n");
ret = -EFAULT;
}
// 读出5字节数据
for (i = 0; i < 5; i++)
{
if (dht11_read_byte(dht,&data[i]))
{
printk("read data err\n");
ret = -EAGAIN;
}
}
if (data[4] != (data[0]+data[1]+data[2]+data[3]))
{
printk("check data failed\n");
ret = -EAGAIN;
}
dht->humidity = data[0];
dht->temperature = data[2];
return 0;
}
c)与Linux驱动框架有关代码
static int __init dht11_init(void)
{
int ret = 0;
uint16_t humidity,temperature;
// 1、获取设备节点:gpioled
g_dht11_device.device_node = of_find_node_by_path("/dht11");
if(g_dht11_device.device_node == NULL) {
printk("dht11 node not find!\r\n");
return -EINVAL;
} else {
printk("dht11 node find!\r\n");
}
// 2、 获取设备树中的gpio属性,得到LED所使用的LED编号
g_dht11_device.gpio = of_get_named_gpio(g_dht11_device.device_node, "dht11-gpio", 0);
if(g_dht11_device.gpio < 0) {
printk("can't get dht11 gpio");
return -EINVAL;
}
printk("dht11 gpio num = %d\r\n", g_dht11_device.gpio);
// 3、设置GPIO1_IO03为输出,并且输出高电平
gpio_request(g_dht11_device.gpio, "dht11_gpio"); // 请求IO
printk("dht11 gpio : %d\n",g_dht11_device.gpio);
//gpio_direction_input(g_dht11_device.gpio); // 设置为输入
ret = gpio_direction_output(g_dht11_device.gpio, 1);
if(ret < 0) {
printk("can't set gpio!\r\n");
}
// 注册字符设备驱动
// 4、创建设备号
if (g_dht11_device.major) { // 定义了设备号
g_dht11_device.dev_id = MKDEV(g_dht11_device.major, 0);
register_chrdev_region(g_dht11_device.dev_id, 1, DHT11_NAME);
} else { // 没有定义设备号
alloc_chrdev_region(&g_dht11_device.dev_id, 0, 1, DHT11_NAME); // 申请设备号
g_dht11_device.major = MAJOR(g_dht11_device.dev_id); // 获取分配号的主设备号
g_dht11_device.minor = MINOR(g_dht11_device.dev_id); // 获取分配号的次设备号
}
printk("gpioled major=%d,minor=%d\r\n",g_dht11_device.major, g_dht11_device.minor);
// 5、初始化cdev、添加一个cdev
g_dht11_device.cdev.owner = THIS_MODULE;
cdev_init(&g_dht11_device.cdev, &dht11_fops);
cdev_add(&g_dht11_device.cdev, g_dht11_device.dev_id, 1);
// 6、创建类
g_dht11_device.class = class_create(THIS_MODULE, DHT11_NAME);
if (IS_ERR(g_dht11_device.class)) {
return PTR_ERR(g_dht11_device.class);
}
// 7、创建设备
g_dht11_device.device = device_create(g_dht11_device.class, NULL, g_dht11_device.dev_id, NULL, DHT11_NAME);
if (IS_ERR(g_dht11_device.device)) {
return PTR_ERR(g_dht11_device.device);
}
// 8、初始化定时器
init_timer(&g_dht11_device.timer);
g_dht11_device.timer.function = timer_func;
g_dht11_device.timer.data = (volatile unsigned long)(&g_dht11_device);
g_dht11_device.timer.expires = jiffies + msecs_to_jiffies(1000);//设定超时时间,1000代表1秒
add_timer(&g_dht11_device.timer);//添加定时器,定时器开始生效
//仅用于驱动测试
dht11_read_data(&g_dht11_device);
printk("humidity: %d temperature: %d \n",g_dht11_device.humidity,g_dht11_device.temperature);
return 0;
}
static void __exit dht11_exit(void)
{
del_timer(&g_dht11_device.timer); //删除定时器
gpio_free(g_dht11_device.gpio);
// 注销字符设备驱动
cdev_del(&g_dht11_device.cdev);// 删除cdev
unregister_chrdev_region(g_dht11_device.dev_id, 1); // 注销设备号
device_destroy(g_dht11_device.class, g_dht11_device.dev_id);
class_destroy(g_dht11_device.class);
}
module_init(dht11_init);
module_exit(dht11_exit);
MODULE_LICENSE("GPL");
d)poll相关程序
dht11使用了poll机制,这里用定时器每隔一定时间读取dht11的数据,将其保存下来,并通过poll函数通知应用程序。相关代码如下。
DECLARE_WAIT_QUEUE_HEAD(dht11_poll_queue); //定义一个队列
//定时器回调函数,每一秒执行一次
static void timer_func(unsigned long arg)
{
struct dht11_device *dht = (struct dht11_device *)arg;
dht11_read_data(dht); //每秒更新一次传感器数据
dht->timer.function = timer_func;
dht->timer.expires = jiffies + msecs_to_jiffies(1000);//设定超时时间,1000代表1秒
add_timer(&(dht->timer));//添加定时器,定时器开始生效
//通知poll,数据可以读取
dht->data_flag= 1;
//printk("humidity: %d temperature: %d \n",dht->humidity,dht->temperature);
}
static unsigned int dht11_poll (struct file * fp, poll_table * wait)
{
unsigned int mask = 0;
poll_wait(fp,&dht11_poll_queue, wait);
if(g_dht11_device.data_flag == 1){
g_dht11_device.data_flag = 0;
mask |= POLLOUT;
}
return mask;
}
static ssize_t dht11_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int err = 0;
struct dht11_device *dht = filp->private_data;
uint16_t data[] = { dht->humidity, dht->temperature };
err = copy_to_user(buf, data, sizeof(data));
return err;
}
// 设备操作函数
static const struct file_operations dht11_fops = {
.owner = THIS_MODULE,
.open = dht11_open,
.read = dht11_read,
.release = dht11_release,
.poll = dht11_poll,
};
应用层使用poll机制,等待dht11能够读取。使用read系统调用读取dht11的数据。
insmod进内核之后,显示的结果。
i2c接口的0.96寸oled在之前的博文里面已经讲解过。具体请参考博文:linux驱动系列学习之i2c子系统(四)
这里面想说下,目前手里面只有i2c接口的oled,测试了一下写的驱动,刷新一帧挺慢的,后面考虑换成spi接口的oled,加上缓存区处理,应该能将显示效果提高不少。
这里给出dht11读取线程代码,只将其中一部分写出。本次poll函数只有一个dht11设备需要等待操作,这里给出5个pollfd,演示下poll等待多个文件可操作。
char* file_path = "/dev/htq-dht11";
int dht11_thread(void* p)
{
uint16_t data[2] = { 0 };
struct pollfd fds[5];
int rc = 0;
int i = 0;
int fd = open(file_path, O_RDWR);
printf("fd: %d\n",fd);
fds[0].fd = fd;
fds[0].events = POLLOUT;
while(1){
rc = poll(fds, 5, 3 * 1000);
printf("rc: %d\n",rc);
for(i = 0;i < 5;i++){
if (fds[i].revents == POLLOUT) //有数据可以读取
{
printf("i: %d revents: %d\n",i,fds[i].revents);
read(fds[i].fd,data,sizeof(data));
printf("humidity: %d temperature: %d \n",data[0],data[1]);
}
}
sleep(1);
}
}
这里,使用ioctl控制oled显示数据到某个位置。等待读取dht11数据,并将其显示出来。
#define OLED_PATH "/dev/oled_htq"
#define TEMP_HUMI_PATH "/dev/htq-dht11"
#define POLL_CNT 1
int show_temperature_humidity(int _oled_fd, int x, int y, double temperature, double humidity);
int oled_fd = 0, temp_humi_fd = 0;
int oled_thread(void* p)
{
uint16_t data[2] = { 0 };
struct pollfd fds[POLL_CNT];
double temperature, humidity;
int i = 0,rc = 0;
oled_fd = open(OLED_PATH, O_RDWR);
if (oled_fd < 0) {
printf("open %s", OLED_PATH);
fflush(stdout);
perror(" ");
return 0;
}
temp_humi_fd = open(TEMP_HUMI_PATH, O_RDWR);
if (temp_humi_fd < 0) {
printf("open %s", TEMP_HUMI_PATH);
fflush(stdout);
perror(" ");
return 0;
}
printf("oled_fd: %d\n",oled_fd);
printf("temp_humi_fd: %d\n",temp_humi_fd);
fds[0].fd = temp_humi_fd;
fds[0].events = POLLOUT;
while(1){
rc = poll(fds, POLL_CNT, 3 * 1000);
for(i = 0;i < POLL_CNT;i++){
if (fds[i].revents == POLLOUT) //有数据可以读取
{
//printf("i: %d revents: %d\n",i,fds[i].revents);
read(fds[i].fd,data,sizeof(data));
//printf("humidity: %0.2f temperature: %0.2f \n",data[0],data[1]);
humidity = data[0];
temperature = data[1];
show_temperature_humidity(oled_fd,0,0,temperature,humidity);
}
}
sleep(1);
}
close(oled_fd);
close(temp_humi_fd);
return 0;
}
int show_temperature_humidity(int _oled_fd, int x, int y, double temperature, double humidity)
{
uint8_t cur_x = 0, cur_y = 0;
uint32_t data = 0;
char buffer[168] = { 0 }; //最多显示168个字符
if(_oled_fd <= 0) return -1;
if(x > 126 || y > 6) return -2;
cur_x = x;
cur_y = y;
data = cur_y;
data <<= 8;
data |= cur_x;
ioctl(_oled_fd,13,data); //13是自定的设置oled的显示(x,y)
memset(buffer,0,sizeof(buffer));
sprintf(buffer,"temperature: %0.2f",temperature);
write(_oled_fd,buffer,strlen(buffer));
cur_x = x;
cur_y = y + 1;
data = cur_y;
data <<= 8;
data |= cur_x;
ioctl(_oled_fd,13,data);
memset(buffer,0,sizeof(buffer));
sprintf(buffer,"humidity: %0.2f",humidity);
write(_oled_fd,buffer,strlen(buffer));
return 0;
}
运行代码,在oled上面显示温湿度数据。
mqtt协议应用十分广泛,本次使用onenet平台演示功能。具体怎么在onenet上搭建mqtt环境,请参考其他博主的文章。本博文使用的多协议接入这个。使用的是onenet自身提供的MQTT协议API传输数据,在demo里面,有三个变量需要根据自己搭建的环境进行修改。分别是
char* prjid = "XX1154"; //project_id
char* auth_info = "htqmqtt2"; //authoriz info
char* devid = "XX9174017"; //device_id
这三个变量唯一确定一个设备。之后使用响应的API进行传输数据,这里,将主要的代码放下。
int mqtt_thread(void *p)
{
int ret = 0;
int err, flags;
int keep_alive = 1200;
struct MqttSampleContext smpctx[1],*ctx = smpctx;
ctx->host = "183.230.40.39";
ctx->port = 6002;
ret = MqttSample_Init(smpctx);
printf("MqttSample_Init ret: %d\n",ret);
MqttBuffer_Init(ctx->mqttbuf);
ctx->mqttfd = MqttSample_CreateTcpConnect(ctx->host, ctx->port);
if(ctx->mqttfd < 0) {
return -1;
}
ctx->mqttctx->read_func_arg = (void*)(size_t)ctx->mqttfd;
ctx->mqttctx->writev_func_arg = (void*)(size_t)ctx->mqttfd;
err = Mqtt_PackConnectPkt(ctx->mqttbuf, keep_alive, devid, 1,
NULL, NULL, 0,
MQTT_QOS_LEVEL0, 0, prjid,
auth_info, strlen(auth_info));
printf("Mqtt_PackConnectPkt ret: %d\n",err);
ret = Mqtt_SendPkt(ctx->mqttctx, ctx->mqttbuf, 0); //发送数据包,将数据发送到onennet
printf("Mqtt_SendPkt ret: %d\n",ret);
ret = Mqtt_RecvPkt(ctx->mqttctx);
printf("Mqtt_RecvPkt ret: %d\n",ret);
uint32_t size = 0
int retain = 0;
int own = 1;
char send_buffer[1024];
double temperature = 21.2, humidity = 61.2;
int i = 0;
while(1){
MqttBuffer_Init(ctx->mqttbuf);
memset(send_buffer,0,sizeof(send_buffer));
sprintf(send_buffer,"[{\"temperature\":%0.2f,\"humidity\":%0.2f}]", temperature, humidity);
temperature += 0.1;
humidity += 0.2;
size = strlen(send_buffer);
ret = Mqtt_PackDataPointByString(ctx->mqttbuf, g_pkt_id++, 0, kTypeSimpleJsonWithoutTime, send_buffer, size, MQTT_QOS_LEVEL0, retain, own);
printf("Mqtt_RecvPkt ret: %d\n",ret);
ret = Mqtt_SendPkt(ctx->mqttctx, ctx->mqttbuf, 0);
printf("Mqtt_RecvPkt ret: %d\n",ret);
sleep(1);
}
MqttBuffer_Destroy(smpctx->mqttbuf);
Mqtt_DestroyContext(smpctx->mqttctx);
if(smpctx->epfd >= 0) {
close(smpctx->epfd);
smpctx->epfd = -1;
}
if(smpctx->mqttfd >= 0) {
close(smpctx->mqttfd);
smpctx->mqttfd = -1;
}
return 0;
}
mqtt传输到onenet平台上显示的相应结果。发送的数据是随便发送的,这个是之前进行测试。
本项目做的功能十分简单,只是把之前学的做个简单的总结。里面也有许多不足的地方,如oled显示部分,应该使用GRAM现存,使用定时器每隔一段时间刷新一次,这样显示的效果会更好。在应用层应该使用QT开发界面。下面有时间的话,会进行一定的修改