一、需求:
四路风扇分别通过PA6\PG9\PG11\PG12四个脚输出pwm信号,控制风扇风速。但是芯片这4个脚没用硬件PWM功能,所以必须使用io口模拟pwm时序。 主要通过高精度定时器hrtimer去模拟pwm时序
二、功能实现
1、dts文件注册pwm设备
gpio-pwms { compatible = "gpio-pwms"; pinctrl-names = "default"; pwm1 { label = "pwm1"; gpios = <&pio 0 6 GPIO_ACTIVE_HIGH>; //GPIO6 ---> PA6 }; pwm2 { label = "pwm2"; gpios = <&pio 6 9 GPIO_ACTIVE_HIGH>; //GPIO201 ---->PG9 }; pwm3{ label = "pwm3"; gpios = <&pio 6 11 GPIO_ACTIVE_HIGH>; //GPIO203 ----->PG11 }; pwm4{ label = "pwm4"; gpios = <&pio 6 12 GPIO_ACTIVE_HIGH>; //GPIO204 ----->PG12 }; };
2、驱动编写
(1)解析dts文件中的数据
static struct gpio_pwms_platform_data * gpio_pwms_get_devtree_pdata(struct device *dev) { struct device_node *node, *pp; struct gpio_pwms_platform_data *pdata; struct pwm_chip *pwm; int error; int npwms; int i = 0; node = dev->of_node; if (!node) return NULL; npwms = of_get_child_count(node); //获取dts文件中pwm结点的个数 if (npwms == 0) return NULL; pdata = devm_kzalloc(dev, sizeof(*pdata) + npwms * (sizeof *pwm),GFP_KERNEL); if (!pdata) { error = -ENOMEM; goto err_out; } pdata->pwms = (struct pwm_chip *)(pdata + 1); pdata->npwms = npwms; for_each_child_of_node(node, pp) { enum of_gpio_flags flags; if (!of_find_property(pp, "gpios", NULL)) { pdata->npwms--; printk( "Found pwm without gpios\n"); continue; } pwm = &pdata->pwms[i++]; pwm->gpio = of_get_gpio_flags(pp, 0, &flags); //获取每个pwm的gpio printk("pwm->gpio=%d,flags=%d",pwm->gpio,flags); if (pwm->gpio < 0) { error = pwm->gpio; if (error != -ENOENT) { if (error != -EPROBE_DEFER) dev_err(dev, "Failed to get gpio flags, error: %d\n", error); return ERR_PTR(error); } } else { pwm->active_low = flags ; } pwm->desc = of_get_property(pp, "label", NULL); //获取label的字串 } if (pdata->npwms == 0) { error = -EINVAL; goto err_out; } return pdata; err_out: return ERR_PTR(error); }
(2)gpio_demo_probe函数主要用于创建pwm设备和class,分别给四个pwm设备分配主设备和次设备号,并且设置io口的输入输出。
static int gpio_demo_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; int error; int i,ret=0; unsigned int gpio; struct pwm_chip *gpwm = NULL; pdata = pdev->dev.platform_data; if (!pdata) { pdata = gpio_pwms_get_devtree_pdata(dev); //获取dts中定义设备树的数据 if (IS_ERR(pdata)) return PTR_ERR(pdata); if (!pdata) { printk( "missing platform data\n"); return -EINVAL; } } gloabl_pwms_dev = devm_kzalloc(dev, pdata->npwms * sizeof(struct pwm_chip), GFP_KERNEL); if (!gloabl_pwms_dev) { printk("no memory for gloabl_pwms_dev data\n"); return -ENOMEM; } memcpy(gloabl_pwms_dev, pdata->pwms, pdata->npwms * sizeof(struct pwm_chip)); for(i=0;i<pdata->npwms;i++) {
//申请主设备和此设备号,分配了cdev结构,注册了驱动的操作方法集 gpwm = &gloabl_pwms_dev[i]; gpwm->devno = MKDEV(pmajor, i); register_chrdev_region(gpwm->devno , 1, gpwm->desc); gpwm->cdev = cdev_alloc(); gpwm->cdev->owner = THIS_MODULE; cdev_init(gpwm->cdev,&pwm_fops); cdev_add(gpwm->cdev,gpwm->devno,1); } pwm_class = class_create(THIS_MODULE,PWM_CLASS_NAME); //创建class gpio-pwm if(IS_ERR(pwm_class)){ printk("debug:error class_create\n"); ret = PTR_ERR(pwm_class); goto err_class_error; } for (i = 0; i < pdata->npwms; i++) { gpwm = &gloabl_pwms_dev[i]; gpio = gpwm->gpio; gpwm->dev = device_create(pwm_class,NULL,gpwm->devno,NULL,"pwm%d",i+1); //创建pwm设备 if(IS_ERR(gpwm->dev)){ printk("debug:error device_create\n"); ret = PTR_ERR(gpwm); goto err_class_error; } else { printk("pwm_device_create\n"); } if(!gpio_is_valid(gpio)) printk("debug:invalid gpio,gpio=0x%x\n", gpio); error = gpio_direction_output(gpio, !((gpwm->active_low == OF_GPIO_ACTIVE_LOW) ? 0 : 1)); //设置io口为输出即默认电平 if (error) { printk( "unable to set direction on gpio %u, err=%d\n", gpio, error); return error; } //设置默认pwm周期和高电平时间 gpwm->period = 40000; gpwm->duty = 20000; //申请device error = devm_gpio_request(dev, gpio,gpwm->desc); if (error) { printk( "unable to request gpio %u, err=%d\n", gpio, error); goto err_device_create; } else { printk("successed to request gpio\n"); gpwm->status = PWM_DISABLE; } } return 0; err_device_create: device_destroy(pwm_class,gpwm->devno); err_class_error: class_destroy(pwm_class); return ret; }
注册了字符设备后,/dev/目录下会生成pwm1\pwm2\pwm3\pwm4四个字符设备。可以在应用层去对设备进行读写操作,会调用到驱动下的这几个函数。
const struct file_operations pwm_fops = { .open = pwm_drv_open, .write = pwm_drv_write, .read = pwm_drv_read, .unlocked_ioctl = pwm_drv_ioctl, .release = pwm_drv_close, };
(3)pwm_drv_ioctl函数会对应用层发过来的指令进行响应.
//command
#define PWM_PERIOD_SET _IOW(‘A‘, 1, unsigned long) #define PWM_DUTY_SET _IOW(‘A‘, 2, unsigned long) #define PWM_START _IOW(‘A‘, 3, unsigned long)
long pwm_drv_ioctl(struct file *filep, unsigned int cmd, unsigned long arg) { int ret = 0,minornum=0; struct inode * ginode = NULL; struct pwm_chip * pwm_dev = NULL; ginode = file_inode(filep); minornum= iminor(ginode); pwm_dev = &gloabl_pwms_dev[minornum]; printk("pwm_drv_ioctl.minornum=%d...gpio=%d.period=%ld..duty=%ld..\n",minornum,pwm_dev->gpio,pwm_dev->period,pwm_dev->duty); switch(minornum) { case 0: pwm1_dev = &gloabl_pwms_dev[minornum]; break; case 1: pwm2_dev = &gloabl_pwms_dev[minornum]; break; case 2: pwm3_dev = &gloabl_pwms_dev[minornum]; break; case 3: pwm4_dev = &gloabl_pwms_dev[minornum]; break; default: break; } switch(cmd) { case PWM_PERIOD_SET : if(0 == minornum) { pwm1_dev->period = arg; } else if(1 == minornum) { pwm2_dev->period = arg; } else if(2 == minornum) { pwm3_dev->period = arg; } else if(3 == minornum) { pwm4_dev->period = arg; } break; case PWM_DUTY_SET : if(0 == minornum) { pwm1_dev->duty = arg; } else if(1 == minornum) { pwm2_dev->duty = arg; } else if(2 == minornum) { pwm3_dev->duty = arg; } else if(3 == minornum) { pwm4_dev->duty = arg; } break; case PWM_START : if(0 == minornum) { if(pwm1_dev->status == PWM_DISABLE){ // start timer pwm_gpio_start(minornum); pwm1_dev->status = PWM_ENABLE; }else{ printk("debug:pwm1_gpio aready work\n"); } } else if(1 == minornum) { if(pwm2_dev->status == PWM_DISABLE){ // start timer pwm_gpio_start(minornum); pwm2_dev->status = PWM_ENABLE; }else{ printk("debug:pwm2_gpio aready work\n"); } } else if(2 == minornum) { if(pwm3_dev->status == PWM_DISABLE){ // start timer pwm_gpio_start(minornum); pwm3_dev->status = PWM_ENABLE; }else{ printk("debug:pwm3_gpio aready work\n"); } } else if(3 == minornum) { if(pwm4_dev->status == PWM_DISABLE){ // start timer pwm_gpio_start(minornum); pwm4_dev->status = PWM_ENABLE; }else{ printk("debug:pwm4_gpio aready work\n"); } } break; default : ret = -1; break; } return 0; }
(4)hrtimer精准定时器模拟pwm信号
- 初始化定时器,是指hrtimer1_handler为回调函数,hrtimer_start激活回调函数。
hrtimer_init(&pwm1_dev->mytimer,CLOCK_MONOTONIC,HRTIMER_MODE_REL); pwm1_dev->mytimer.function = hrtimer1_handler; pwm1_dev->kt = ktime_set(0, pwm1_dev->period-pwm1_dev->duty); hrtimer_start(&pwm1_dev->mytimer,pwm1_dev->kt,HRTIMER_MODE_REL);
- 初始化完之后会调用hrtimer1_handler函数,在回调函数会判断io口的电平高低,然后使用ktime_set设置到期时间,如果io为低电平,如果duty不为0,则拉高,ktime_set设置高电平的时间为duty,hrtimer_forward_now函数会等待duty纳秒,然后再执行回调函数hrtimer1_handler,再进行判断,如此循环。使用hrtimer_cancel函数去取消hrtimer.
我们需要四个定时器去模拟pwm,故要写四个回调函数模拟。
注意:根据实际测量,这里gpio_get_value获取到gpio的值,1为低电平,0为高电平。
static enum hrtimer_restart hrtimer1_handler(struct hrtimer *timer) { if (gpio_get_value(pwm1_dev->gpio) == 1) { // There is no need to pull down when the duty cycle is 100% if (pwm1_dev->duty != 0) { gpio_set_value(pwm1_dev->gpio, 0); pwm1_dev->kt = ktime_set(0, pwm1_dev->duty); } // timer overflow hrtimer_forward_now(&pwm1_dev->mytimer, pwm1_dev->kt); } else { // There is no need to pull up when the duty cycle is 0 if (pwm1_dev->duty != pwm1_dev->period) { gpio_set_value(pwm1_dev->gpio, 1); pwm1_dev->kt = ktime_set(0, pwm1_dev->period-pwm1_dev->duty); } // timer overflow hrtimer_forward_now(&pwm1_dev->mytimer, pwm1_dev->kt); } return HRTIMER_RESTART; }
(5)设备注册
static struct of_device_id gpio_demo_of_match[] = { {.compatible = "gpio-pwms"}, {}, } MODULE_DEVICE_TABLE(of, gpio_demo_of_match); static struct platform_driver gpio_demo_driver = { .probe = gpio_demo_probe, .driver = { .name = "gpio-pwms", .owner = THIS_MODULE, .of_match_table = of_match_ptr(gpio_demo_of_match), } }; static int __init gpio_demo_init(void) { return platform_driver_register(&gpio_demo_driver); } static void __exit gpio_demo_exit(void) { int i; struct pwm_chip *gpwm = NULL; for(i=0;i<pdata->npwms;i++ ) { gpwm = &gloabl_pwms_dev[i]; gpio_set_value(gpwm->gpio, 1); gpio_free(gpwm->gpio); device_destroy(pwm_class,gpwm->devno); cdev_del(gpwm->cdev); unregister_chrdev_region(gpwm->devno,1); hrtimer_cancel(&gpwm->mytimer); kfree(gpwm); } class_destroy(pwm_class); return platform_driver_unregister(&gpio_demo_driver); } late_initcall(gpio_demo_init); module_exit(gpio_demo_exit); // add by SouthLj 2019-0924 end MODULE_LICENSE("GPL"); MODULE_AUTHOR("SouthLj");
注册完pwm设备和gpio_pwms class就可以看到如下目录:
可以看到设备的主、次设备号:
/dev目录下也可以看到四个设备,应用层就会通过ioctl对pwm设备写数据到驱动。
驱动的pwm_drv_ioctl函数根据应用层传过来的指令设置pwm周期,duty数据调整pwm信号。
luci界面中设置风扇的数据后,会将数据更新到/etc/config/cgminer配置文件中,然后重新启动cgminer(即执行/etc/init.d/cgminer脚本),脚本中会将风扇风速最大最小值、风速默认值、风速自动控制、预启动时间以及预启动时间内风扇的风速,通过传参传给cgminer.
AVA9_OPTIONS=" --fan-limit $_fan_min-$_fan_max $VOLT_OFFSET" PARAMS=" --lowmem $AVA9_OPTIONS $POOL1 $POOL2 $POOL3 --api-allow $_aa --api-listen $_mo --fan-ctrl $_fan_ctrl $PRE_BOOT --pwm-default $_pwm_default" NTP_POOL="-p 0.openwrt.pool.ntp.org -p 1.openwrt.pool.ntp.org -p 3.openwrt.pool.ntp.org -p 4.openwrt.pool.ntp.org" ASIA="-p 1.cn.pool.ntp.org -p 3.asia.pool.ntp.org -p 2.asia.pool.ntp.org" # _ntp_enable: openwrt, asia, globle if [ "$_ntp_enable" == "asia" ]; then NTP_POOL="${ASIA}" fi if [ ! -f /tmp/cgminer-ntpd-done -a "$_ntp_enable" != "disable" ]; then while [ "$NTPD_RET" != "0" ]; do ntpd -d -n -q -N ${NTP_POOL} NTPD_RET=$? done touch /tmp/cgminer-ntpd-done fi # Make sure udevd run before cgminer start UDEVDCNT=`pidof udevd | wc -w` if [ "$UDEVDCNT" == "0" ]; then mkdir -p /run udevd --daemon fi sleep 2 start-stop-daemon -S -x $APP -p $PID_FILE -m -b -- $PARAMS
pwm占空比设到20%,pwm波形不稳定,导致风扇有顿挫感。nano给的软件默认最小占空比为30%.
cgminer.c中通过参数判断会执行相应的函数:
char *set_avalon9_fan_auto_control(char *arg) { int ret=1,autocontrol=0; ret = sscanf(arg, "%d", &autocontrol); printf("autocontrol=%d\n",autocontrol); if (ret < 1) return "No value passed to avalon9-fan-auto-control"; opt_avalon9_fan_auto_control = autocontrol; return NULL; }
//风扇预启动设置逻辑是,如果风扇自动控制开关为enable,控制板上电开机后,则会根据
预启动速度跑,跑的时间是预启动时间,时间过后,风扇风速会按照默认风速转。预启动
设置只在上电第一次有效。
char *set_avalon9_fan_pre_boot_setup(char *arg) { int ret=1,prebootime =1,prebootfan =100; ret = sscanf(arg, "%d-%d", &prebootfan,&prebootime); printf("prebootime=%d,prebootfan=%d\n",prebootime,prebootfan); if (prebootfan < 0 || prebootfan> 100 || prebootime < 0 || prebootime > 10) return "Invalid value passed to boot_setup"; if (ret < 1) return "No value passed to avalon9-fan-pre-boot-setup"; prebootflag = 1; opt_avalon9_fan_pre_boottim = prebootime; opt_avalon9_fan_pre_bootfan = prebootfan; return NULL; } void open_pwm_device(float pwmval) { int fd; fd = open("/dev/pwm1",O_RDWR ); if(fd < 0) { printf("failed to open pwm1 failed!\n"); } ioctl(fd,PWM_PERIOD_SET,40000); // 10 000 00ns = 1ms 10 00ns = 1us ioctl(fd,PWM_DUTY_SET,(int)(40000*((float)pwmval/100))); ioctl(fd,PWM_START,1); close(fd); fd = open("/dev/pwm2",O_RDWR); if(fd < 0) { printf("failed to open pwm2!\n"); } ioctl(fd,PWM_PERIOD_SET,40000); // 10 000 000ns = 10ms ioctl(fd,PWM_DUTY_SET,(int)(40000*((float)pwmval/100))); ioctl(fd,PWM_START,1); close(fd); fd = open("/dev/pwm3",O_RDWR); if(fd < 0) { printf("failed to open pwm3 failed!\n"); } ioctl(fd,PWM_PERIOD_SET,40000); // 10 000 000ns = 10ms ioctl(fd,PWM_DUTY_SET,(int)(40000*((float)pwmval/100))); ioctl(fd,PWM_START,1); close(fd); fd = open("/dev/pwm4",O_RDWR); if(fd < 0) { printf("failed to open pwm4! \n"); } ioctl(fd,PWM_PERIOD_SET,40000); // 10 000 000ns = 10ms ioctl(fd,PWM_DUTY_SET,(int)(40000*((float)pwmval/100))); ioctl(fd,PWM_START,1); close(fd); } char *set_avalon9_fan_pwm_default(char *arg) { int pwmval; int ret=1; // int delaytime = 90000000; ret = sscanf(arg, "%d", &pwmval); printf("\n...........set_avalon9_fan_default_pwm....pwmval=%d..opt_avalon9_fan_auto_control=%d...\n",pwmval,opt_avalon9_fan_auto_control); if (ret < 1) return "No value passed to avalon9-fan-default-pwm"; if(pwmval<opt_avalon9_fan_min) { pwmval =opt_avalon9_fan_min; } else if(pwmval>opt_avalon9_fan_max) { pwmval =opt_avalon9_fan_max; } if(opt_avalon9_fan_auto_control == 1) { if(opt_avalon9_fan_pre_bootfan<opt_avalon9_fan_min) { opt_avalon9_fan_pre_bootfan =opt_avalon9_fan_min; } else if(opt_avalon9_fan_pre_bootfan>opt_avalon9_fan_max) { opt_avalon9_fan_pre_bootfan =opt_avalon9_fan_max; } if(prebootflag==1) { prebootflag==0; open_pwm_device(opt_avalon9_fan_pre_bootfan); cgsleep_ms(opt_avalon9_fan_pre_boottim*60000); } open_pwm_device(pwmval); } else { open_pwm_device(pwmval); } return NULL; }
通过cat /sys/class/kernel/gpio命令查看四个pwm口的电平高低。
static struct of_device_id gpio_demo_of_match[] = { {.compatible = "gpio-pwms"}, {},} MODULE_DEVICE_TABLE(of, gpio_demo_of_match); static struct platform_driver gpio_demo_driver = { .probe = gpio_demo_probe, .driver = {.name = "gpio-pwms",.owner = THIS_MODULE,.of_match_table = of_match_ptr(gpio_demo_of_match), }}; static int __init gpio_demo_init(void){ return platform_driver_register(&gpio_demo_driver);} static void __exit gpio_demo_exit(void){int i; struct pwm_chip *gpwm = NULL;for(i=0;i<pdata->npwms;i++ ){gpwm = &gloabl_pwms_dev[i];gpio_set_value(gpwm->gpio, 1);gpio_free(gpwm->gpio);device_destroy(pwm_class,gpwm->devno);cdev_del(gpwm->cdev);unregister_chrdev_region(gpwm->devno,1);hrtimer_cancel(&gpwm->mytimer);kfree(gpwm);}class_destroy(pwm_class); return platform_driver_unregister(&gpio_demo_driver);} late_initcall(gpio_demo_init);module_exit(gpio_demo_exit);// add by SouthLj 2019-0924 end MODULE_LICENSE("GPL");MODULE_AUTHOR("SouthLj");