嵌入式爱好者

查看: 28354|回复: 1

[评测] 【飞凌嵌入式 OK3399-C+开发板试用体验】本地编译内核源码&DHT11驱动编译

[复制链接]

4

主题

4

帖子

64

积分

AM335x通行证i.MX6UL通行证i.MX RT通行证XX18通行证

扫一扫,手机访问本帖
发表于 2020-9-6 18:06:09 | 显示全部楼层 |阅读模式
本帖最后由 donatello1996 于 2020-9-6 18:36 编辑

飞凌厂商提供的Linux源码资料压缩包实际上是多合一源码,除了最常用的内核源码以外,还有诸多额外内容比如交叉编译链,Linux烧录工具(卡刷+线刷),生成系统镜像img的脚本,uboot源码,应用程序代码等诸多内容,这些不同内容面向不同层次的开发者,比如开发应用层的只看应用程序代码即可,开发驱动的只看内核源码和交叉编译链即可,开发uboot的只看uboot源码即可,产品部门相关的只需要会用工具即可。内核源码为主目录的kernel文件夹,交叉编译链为主目录的prebuilt文件夹,平时如果执行厂商提供的build_ubuntu.sh脚本,实际上不只是编译内核和驱动(boot.img,**.ko),还有生成必须要的分区镜像如recovery.img,userdata.img等:
21.jpg

我的想法是直接使用板子本地编译,减少内核源码不兼容的风险,那么就只需要保留kernel文件夹的内容即可,kernel文件夹就是OK3399板子的全部内核源码,根目录有Makefile可以直接编译,因此准备一个足够大的U盘,格式化为EXT4格式,将整个kernel文件夹都存进去,改名为OK3399文件夹,然后把U盘插到板子上,将U盘挂载出来:
  1. mount /dev/sda /mnt
  2. cd /mnt
复制代码
22.jpg


然后是在板子上安装64位aarch编译工具,通常Ubuntu系统都会自带的:
  1. apt install aarch64-linux-gnu-gcc
复制代码
23.jpg

那么就可以直接在板上进行本地内核编译了:
  1. make clean
  2. make rockchip_linux_defconfig
  3. make
复制代码


-这里貌似是不能用-j2 -j4多线程编译的,效果跟单线程编译差不多
-编译时长1~2小时
-编译过程缺失了一些必要的软件库如openssl等,要安装上去
  1. apt install libssl-dev
  2. apt install libssl1.0.0= 1.0.2g-1ubuntu4.16
复制代码
19.jpg 20.jpg

-/mnt/OK3399/scripts/gcc_wrapper.py文件对编译没有任何作用,但是里面有句关于utf8的语句会导致编译报错,这个文件完全可以删掉,或者把有关utf8的语句注释掉即可。

编译完毕之后内核目录/mnt/OK3399就可以给各种需要内核源码的驱动代码编译了,比如自己写的GPIO驱动,现成的USB网卡驱动,4G模块驱动等,我手头上有一个腾达网卡,主控是RTL8192EU,网上也有相应源码,解压缩之后内容是这样的:
24.jpg

修改Makefile文件:
25.jpg 26.jpg

-修改源码目录KSRC,指定为内核根目录
-修改驱动目录,即../drivers/net/wireless
-指定架构ARCH=arm64
-指定交叉编译链aarch64-linux-gnu-gcc

编译:
  1. cd /mnt/RTL8192EU
  2. make clean
  3. make
复制代码
27.jpg
生成8192eu.ko文件可以直接安装:
insmod 8192eu.ko

安装完毕之后ifconfig可以找到一个乱码的网卡:
IMG_20200906_180919.jpg 28.jpg
我这边已经用nmcli来连接WIFI了,分配到了IP地址。

然后是LTE模块,是移远的EC20,miniPCIE接口,可以直接插到板子背面的miniPCIE卡座上:

移远模块虽然是miniPCIE接口,但实际上是走USB协议的,需要编译移远官方提供的USB驱动,并加入到内核中,

第一步是打开drivers/usb/serial/option.c文件,添加移远设备的VID:
  1. #define QUECTEL_VENDOR_ID            0x2c7c
复制代码
29.jpg

然后是EC20/EC25的公用产品ID 0x0125:
  1. #define QUECTEL_PRODUCT_EC25            0x0125
复制代码


继续在此文件drivers/usb/serial/option.c文件中找到option_ids 数组,在此数组里面加入如下内容:
  1. { USB_DEVICE(0x2C7C, 0x0125) }, /* Quectel EC25 */
复制代码
30.jpg

继续在此文件drivers/usb/serial/option.c文件中找到option_probe 函数,在此函数里面添加如下内容:
  1.     if (dev_desc->idVendor == cpu_to_le16(0x05c6) &&
  2.         dev_desc->idProduct == cpu_to_le16(0x9003) &&
  3.         iface_desc->bInte**ceNumber >= 4)
  4.         return -ENODEV;

  5.     if (dev_desc->idVendor == cpu_to_le16(0x05c6) &&
  6.         dev_desc->idProduct == cpu_to_le16(0x9215) &&
  7.         iface_desc->bInte**ceNumber >= 4)
  8.         return -ENODEV;

  9.     if (dev_desc->idVendor == cpu_to_le16(0x2c7c) &&
  10.     iface_desc->bInte**ceNumber >= 4)
  11.     return -ENODEV;
复制代码
31.jpg


继续在此文件drivers/usb/serial/option.c 文件里面找到option_1port_device结构体变量,在里面加入休眠/唤醒函数:
  1. static struct usb_serial_driver option_1port_device = {
  2.     .driver = {
  3.         .owner =    THIS_MODULE,
  4.         .name =        "option1",
  5.     },
  6.     .description       = "GSM modem (1-port)",
  7.     .id_table          = option_ids,
  8.     .num_ports         = 1,
  9.     .probe             = option_probe,
  10.     .open              = usb_wwan_open,
  11.     .close             = usb_wwan_close,
  12.     .dtr_rts       = usb_wwan_dtr_rts,
  13.     .write             = usb_wwan_write,
  14.     .write_room        = usb_wwan_write_room,
  15.     .chars_in_buffer   = usb_wwan_chars_in_buffer,
  16.     .tiocmget          = usb_wwan_tiocmget,
  17.     .tiocmset          = usb_wwan_tiocmset,
  18.     .ioctl             = usb_wwan_ioctl,
  19.     .attach            = option_attach,
  20.     .release           = option_release,
  21.     .port_probe        = usb_wwan_port_probe,
  22.     .port_remove       = usb_wwan_port_remove,
  23.     .read_int_callback = option_instat_callback,
  24. #ifdef CONFIG_PM
  25.     .suspend           = usb_wwan_suspend,
  26.     .resume            = usb_wwan_resume,
  27.     .reset_resume      = usb_wwan_resume,
  28. #endif
  29. };
复制代码



drivers/usb/serial/usb_wwan.c文件的usb_wwan_setup_urb函数添加:

  1. static struct urb *usb_wwan_setup_urb(struct usb_serial_port *port,
  2.                       int endpoint,
  3.                       int dir, void *ctx, char *buf, int len,
  4.                       void (*callback) (struct urb *))
  5. {
  6.     struct usb_serial *serial = port->serial;
  7.     struct urb *urb;

  8.     urb = usb_alloc_urb(0, GFP_KERNEL);    /* No ISO */
  9.     if (!urb)
  10.         return NULL;

  11.     usb_fill_bulk_urb(urb, serial->dev,
  12.               usb_sndbulkpipe(serial->dev, endpoint) | dir,
  13.               buf, len, callback, ctx);
  14.     if (dir == USB_DIR_OUT) {
  15.         struct usb_device_descriptor *desc = &serial->dev->descriptor;
  16.         if (desc->idVendor == cpu_to_le16(0x05C6) && desc->idProduct == cpu_to_le16(0x9090))
  17.             urb->transfer_flags |= URB_ZERO_PACKET;
  18.         if (desc->idVendor == cpu_to_le16(0x05C6) && desc->idProduct == cpu_to_le16(0x9003))
  19.             urb->transfer_flags |= URB_ZERO_PACKET;
  20.         if (desc->idVendor == cpu_to_le16(0x05C6) && desc->idProduct == cpu_to_le16(0x9215))
  21.             urb->transfer_flags |= URB_ZERO_PACKET;
  22.         if (desc->idVendor == cpu_to_le16(0x2C7C))
  23.             urb->transfer_flags |= URB_ZERO_PACKET;
  24.     }
  25.     return urb;
  26. }
复制代码
35.jpg
这样编译生成的内核就支持EC20模块的USB通信部分了。然后就是要进入menuconfig菜单选项那里把USB网卡的配置打开,还有把Gobi上网选项打开即可,这样板子上电就能找到/dev/qcmi设备。
IMG_20200906_180901.jpg 38.jpg

解决了两个网络模块的驱动,接下来可以弄一下较为简单一些的功能,比如驱动GPIO输出高低电平电灯以及驱动DHT11器件,直接对GPIO写入高低电平非常简单,但实际上我人生第一份接触驱动的代码并不是GPIO写入电平,而是比这复杂得多的DHT11驱动,既然DHT11的驱动都能读懂了,那驱动GPIO读写电平当然简单多了。对于GPIO写入电平的函数很简单,一共就3个gpio_request(),gpio_direction_output(),gpio_set_value(),读取则是gpio_request(),gpio_direction_input(),gpio_get_value(),然后对于每个/dev下的驱动文件,操作一共有四个,分别是open()——打开设备文件,read()——从设备中读取数据,wirte()——写入数据到设备,close()——关闭设备文件,其中读写都是以字节为单位进行,也就是传输若干字节(长度可自定义)的unsigned char[]数组类型。比如定义一个设备的文件操作结构体即fops,我们使用板上的GPIO2_A5接口即GPIO69:

  1. static struct file_operations mygpio69_fops = {
  2.     .owner = THIS_MODULE,
  3.         .read = MYGPIO69_read,
  4.         .write = MYGPIO69_write,
  5.         .open = MYGPIO69_open,
  6.         .release = MYGPIO69_close,
  7. };
复制代码



与设备树的mygpio69节点对应:
  1. static const struct of_device_id of_mygpio69_match[] =
  2. {
  3.         { .compatible = "mygpio69", },
  4.     {},
  5. };
复制代码

要使用这个设备树节点,就要先在设备树OK3399.dts里面添加设备树节点:
32.jpg
重新烧录boot.img:
33.jpg
烧录完毕启动就可以在/proc/device-tree里面找到设备树节点,还可以用相同的办法添加一个名为dht11的节点,之后要用到:
34.jpg
36.jpg

添加节点之后就可以在驱动代码里面进行GPIO写电平操作了,复写MYGPIO69_write函数,超级简单:
  1. static int mygpio69_gpio = 69;
  2. //GPIO号

  3. #define MYGPIO69_LOW    gpio_set_value(mygpio69_gpio, 0)
  4. #define MYGPIO69_HIGH   gpio_set_value(mygpio69_gpio, 1)

  5. static ssize_t MYGPIO69_write(struct file *file,const char __user *buf,
  6.                     size_t nbytes, loff_t *ppos)
  7. {
  8.     if(buf[0]==1)
  9.         MYGPIO69_HIGH;
  10.     else if(buf[0]==0)
  11.         MYGPIO69_LOW;
  12. }

  13. 当然了,要正常调用这些函数操作GPIO,是要先初始化GPIO的:

  14.     if (gpio_request(mygpio69_gpio, "mygpio69_gpio"))
  15.     {
  16.         printk("gpio %d request failed!\n", mygpio69_gpio);
  17.         gpio_free(mygpio69_gpio);
  18.         return -ENODEV;
  19.     }
  20.     else
  21.         printk("gpio %d request success!\n", mygpio69_gpio);
复制代码

make生成ko文件之后,就会在/dev目录下生成mygpio69设备文件,可以在应用层代码直接操作:
  1. int main ()
  2. {
  3.     int fd ;
  4.         unsigned char val[4],value;
  5.         fd = open ( "/dev/mygpio69" , O_RDWR) ;
  6.     if ( fd == -1 )
  7.     {
  8.                 perror ( "open /dev/mygpio69 error\n" ) ;
  9.         exit ( -1 ) ;
  10.     }
  11.         printf ( "open /dev/mygpio69 successfully\n" ) ;

  12.         while (1)
  13.     {
  14.             value=0;
  15.             write(fd,&value,1);
  16.             sleep(1);
  17.             value=1;
  18.             write(fd,&value,1);
  19.             sleep(1);
  20.     }

  21.     close ( fd ) ;
  22. }
复制代码


     驱动DHT11单总线温湿度传感器的方式与内核驱动GPIO读写的方式基本无差异,都是通过对/dev目录下的设备文件进行write/read操作,不同的是DHT11的读取需要不断切换GPIO输入输出状态并不断做轮询,其运行层面是驱动层,对于系统来说,驱动层代码的实时性要求和读写优先级都要高于应用层代码,即要将更多的CPU任务时间片资源放到驱动层代码中,也就只有这样,才能对DHT11这种延时响应时间要求是微秒级的单总线器件进行正常读写。废话不多说放上DHT11的驱动源码,这份代码参考了一位开发驱动的朋友@jackeyt,修改了一些小地方:
  1. static u8 DHT11_Read_Data(u16 *temp,u16 *humi)   
  2. {        
  3.         u8 buf[5];
  4.         u8 i;
  5.         DHT11_Rst();
  6.         if(DHT11_Check()==0)
  7.         {
  8.                 for(i=0;i<5;i++)//读取40位数据
  9.                 {
  10.                         buf[i]=DHT11_Read_Byte();
  11.                 }
  12.                 if((buf[0]+buf[1]+buf[2]+buf[3])==buf[4])
  13.                 {
  14.                         *humi=buf[0]<<8|buf[1];
  15.                         *temp=buf[2]<<8|buf[3];
  16.                         printk("buf=%d,%d,%d,%d,%d\n",buf[0],buf[1],buf[2],buf[3],buf[4]);
  17.                 }
  18.         }else return 1;
  19.         return 0;   
  20. }
复制代码


  1. static ssize_t DHT11_read(struct file *file, char __user *buf,
  2.                     size_t nbytes, loff_t *ppos)
  3. {       
  4.         printk("--------------%s--------------\n",__FUNCTION__);
  5.        
  6.         dht11_data Last_dht11_data;
  7.         if(DHT11_Read_Data(&Last_dht11_data.temp,&Last_dht11_data.hum) == 0);//读取温湿度值
  8.         {
  9.                 if (raw_copy_to_user(buf,&Last_dht11_data,sizeof(Last_dht11_data)) )
  10.                 {
  11.                         return EFAULT ;
  12.                 }
  13.         }

  14. }
复制代码
  1. static struct file_operations dht11_fops = {
  2.         .owner = THIS_MODULE,
  3.         .read = DHT11_read,
  4.         .open = DHT11_open,
  5.         .release = DHT11_close,
  6. };
复制代码
  1. static const struct of_device_id of_dht11_match[] = {
  2.         { .compatible = "dht11", },
  3.         {},
  4. };

  5. MODULE_DEVICE_TABLE(of, of_dht11_match);
复制代码
  1. static struct platform_driver dht11_driver = {
  2.         .probe                = dht11_probe,
  3.         .remove                = dht11_remove,
  4.         .shutdown        = dht11_shutdown,
  5.         .driver                = {
  6.                 .name        = "dht11_driver",
  7.                 .of_match_table = of_dht11_match,
  8.         },
  9. };
复制代码


编译生成dht11_drv.ko文件:
  1. insmod dht11_drv.ko
复制代码

插入ko驱动的时候,必须要检查上面的/proc/device-tree设备树列表那有没有正常生成dht11键值,如果没有的话插入会失败,并且无法在/dev下生成dht11字符设备。

做个简单的小实验看下DHT11能否正常读取温度:
  1. typedef struct        DHT11_SENSOR_DATA
  2. {
  3.         unsigned short temp;//温度
  4.         unsigned short hum;//湿度
  5. }dht11_data;

  6. int main ( void )
  7. {
  8.         int fd ;
  9.         int retval ;
  10.         dht11_data Curdht11_data;
  11.         fd = open ( "/dev/dht11" , O_RDONLY) ;
  12.         if ( fd == -1 )
  13.         {
  14.                 perror ( "open dht11 error\n" ) ;
  15.                 exit ( -1 ) ;
  16.         }
  17.         printf ( "open /dev/dht11 successfully\n" ) ;
  18.         sleep ( 2 ) ;
  19.         while ( 1 )
  20.         {
  21.                 sleep ( 1 ) ;
  22.                 retval = read ( fd , &Curdht11_data , sizeof(Curdht11_data) );
  23.                 if ( retval == -1 )
  24.                 {

  25.                         printf ( "read dht11 error" ) ;
  26.                         exit ( -1 ) ;
  27.                 }
  28.                 if(Curdht11_data.temp != 0xffff)
  29.                 printf ( "Temperature:%d.%d C,Humidity:%d.%d %%RH\n",
  30.                          Curdht11_data.temp>>8,
  31.                          Curdht11_data.temp&0xff,
  32.                          Curdht11_data.hum>>8,
  33.                          Curdht11_data.hum&0xff ) ;
  34.         }
  35.         close ( fd ) ;
  36. }
复制代码
40.jpg
如图,读取DHT11数据正常。


生成DHT11的驱动之后我很想在QT工程里面试试看能不能正常驱动,下一帖试试看。


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|小黑屋| 飞凌嵌入式 ( 冀ICP备12004394号-1 )

GMT+8, 2024-12-23 00:12

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表