ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

关于使用iTop-4412制作简易的PWM波形调节器

关于使用iTop-4412制作简易的PWM波形调节器 文章目录一、先看整体思路二、环境与硬件2.1 软硬件环境2.2 用到的引脚与接口三、驱动一LED 字符设备驱动四、驱动二PWM 驱动重点4.1 寄存器与初始化4.2 频率怎么换算五、Qt 界面480×272 小屏怎么排六、Qt 逻辑ADC 在线调频七、踩坑记录这部分最值钱坑 1改了频率蜂鸣器音调纹丝不动坑 2ADC 读数一直抖频率跟着乱跳坑 3程序一启动蜂鸣器就响坑 4LED 驱动一 open 就把灯点亮了坑 5小屏上控件被挤扁八、实测效果九、怎么复现手上有一块 iTop-4412 开发板想用一个完整的小项目把嵌入式 Linux 驱动开发这条链路走通写字符设备驱动 → 写 Qt 界面 → 用 ADC 旋钮在线调参。这篇文章把整个 PWM 波形调节器的实现过程拆开讲包括能直接复制的驱动代码和我实际踩过的 5 个坑。读完你能拿到一套可运行的 LED PWM 字符设备驱动、一个 480×272 小屏的 Qt 界面、以及为什么 PWM 频率改了没反应这类问题的答案。一、先看整体思路一句话概括Qt 界面下发命令 → ioctl 到字符设备驱动 → 驱动操作 GPIO / 定时器 → LED 闪烁、蜂鸣器发声同时 ADC 旋钮的值反过来驱动界面更新频率。系统分四层层次内容作用应用层Qt 程序pwm_adjuster与用户交互显示状态设备节点层/dev/led_ctl、/dev/pwm、/dev/adc应用与驱动之间的统一接口驱动层ledsdev.ko、pwm-driver.ko操作 GPIO 与定时器寄存器硬件层两颗 LED、蜂鸣器、ADC 旋钮最终的现象这样分层的好处是上层只管open / ioctl / close完全不用知道寄存器叫什么名字。二、环境与硬件2.1 软硬件环境项目内容目标板iTop-4412三星 Exynos4412四核 Cortex-A9内核Linux 3.0交叉工具链arm-linux-gcc图形界面Qt 5.3Qt Creator2.2 用到的引脚与接口资源引脚 / 接口说明LED1EXYNOS4_GPL2(0)配置为 GPIO 输出高电平点亮LED2EXYNOS4_GPK1(1)备用用户灯与 LED1 一同初始化蜂鸣器EXYNOS4_GPD0(0)复用为 PWM 输出SFN2由定时器 1 驱动PWM 定时器寄存器基址0x139D0000使用 Timer1TCFG0 / TCFG1 / TCON / TCNTB1 / TCMPB1ADC 旋钮/dev/adc12 位采样读数 0~4095蜂鸣器这一路是复用的GPD0(0) 平时是普通 GPIO要把它设成 SFN2 才会接到 PWM 定时器输出上。这一点后面还会踩坑。三、驱动一LED 字符设备驱动字符设备驱动的套路基本固定申请设备号 → 注册 cdev → 填 file_operations → 建 class/device 自动生成 /dev 节点。LED 驱动只实现了三个方法open、release、unlocked_ioctl。/* ledsdev.c 片段模块加载时申请设备号并注册 */retalloc_chrdev_region(devno,minor,DEVICE_NUM,char_test);majorMAJOR(devno);cdev_init(leds_dev_cdev,leds_dev_fops);leds_dev_cdev.ownerTHIS_MODULE;cdev_add(leds_dev_cdev,devno,DEVICE_NUM);leds_classclass_create(THIS_MODULE,DEVICE_NAME);device_create(leds_class,NULL,devno,NULL,DEVICE_NAME);/* 生成 /dev/led_ctl */核心的亮灭控制放在 ioctl 里用命令号区分操作哪颗灯/* ledsdev.c 片段 */staticlongleds_dev_ioctl(structfile*file,unsignedintcmd,unsignedlongarg){switch(cmd){caseIO_CMD_LED_1:if(arg1)gpio_set_value(LED1,1);elseif(arg0)gpio_set_value(LED1,0);break;caseIO_CMD_LED_2:if(arg1)gpio_set_value(LED2,1);elseif(arg0)gpio_set_value(LED2,0);break;default:printk(KERN_INFOerror cmd number\n);}return0;}命令号定义在头文件里两个命令用同一个幻数x、用序号区分/* leds-ioctl.h */#defineIOC_MAGICx#defineIO_CMD_LED_1_IO(IOC_MAGIC,0)#defineIO_CMD_LED_2_IO(IOC_MAGIC,1)另外open里要把两颗灯配成 GPIO 输出staticintleds_dev_open(structinode*inode,structfile*file){s3c_gpio_cfgpin(LED1,S3C_GPIO_OUTPUT);s3c_gpio_cfgpin(LED2,S3C_GPIO_OUTPUT);gpio_set_value(LED1,1);gpio_set_value(LED2,1);return0;}驱动整体流程如下四、驱动二PWM 驱动重点这一路的关键不是会不会写驱动而是Exynos4412 的 PWM 定时器怎么算频率。4.1 寄存器与初始化模块加载时先申请蜂鸣器引脚再把定时器寄存器映射进内核虚拟地址空间#defineTIMER_BASE0X139D0000#defineTCFG0(void__iomem*)(timer_base0X00)/* 两级预分频 */#defineTCFG1(void__iomem*)(timer_base0X04)/* 分频器选择 */#defineTCON(void__iomem*)(timer_base0X08)/* 控制含 update 位 */#defineTCNTB1(void__iomem*)(timer_base0X0C)/* 计数缓冲决定周期 */#defineTCMPB1(void__iomem*)(timer_base0X10)/* 比较缓冲决定占空比 */gpio_request(BUZZER_PMW_GPIO,DEVICE_NAME);timer_baseioremap(TIMER_BASE,0x20);open时把 GPD0(0) 复用成 PWM 输出并给定时器写初值staticintpwm_dev_open(structinode*inode,structfile*file){s3c_gpio_cfgpin(BUZZER_PMW_GPIO,S3C_GPIO_SFN(2));/* 复用为 PWM */writel(readl(TCFG0)|0xff,TCFG0);/* 预分频 */writel((readl(TCFG1)(~0x0f))|0x04,TCFG1);/* 分频器 */writel(300,TCNTB1);writel(150,TCMPB1);/* 占空比 50% */writel((readl(TCON)(~0x1f))|0X02,TCON);/* 手动更新待命 */return0;}4.2 频率怎么换算预分频和分频确定之后定时器每计一个数对应固定的时间。把这个常数记为NS_IN_1HZ那么计数值 NS_IN_1HZ / 目标频率把计数值写入TCNTB1一半写入TCMPB1就得到 50% 占空比的方波。#defineNS_IN_1HZ(0X4200000/256/16)casePWM_SET_FREQ:if(data0)return-EINVAL;data(NS_IN_1HZ)/data;/* 计数值 周期常数 / 频率 */writel(data,TCNTB1);/* 计数缓冲决定周期 */writel(data1,TCMPB1);/* 比较缓冲占空比 50% */break;驱动对外提供四条命令设备节点命令含义/dev/pwmPWM_SET_PRE设置预分频0~255/dev/pwmPWM_SET_FREQ设置输出频率Hz/dev/pwmPWM_ON/PWM_OFF启动 / 停止输出五、Qt 界面480×272 小屏怎么排开发板屏幕只有 480×272不能照搬桌面程序的布局。我的做法是把信息压成三个区域区域控件作用LED 控制区QDoubleSpinBoxQLCDNumber Start/Stop设置起始频率0.5~10 Hz显示当前频率蜂鸣器控制区QLCDNumber Start/Stop显示当前 PWM 频率不提供手动输入完全由 ADC 决定ADC 显示区4 位QLCDNumber实时显示 ADC 原始值 0~4095布局上 LED 区和蜂鸣器区左右并排ADC 显示放底部主布局用垂直布局、边距和间距都设 4 像素全局字号调到 8 磅。这样安排的原因是横向空间有限左右并排能避免控件被压扁而设置在上、反馈在下也符合使用习惯。六、Qt 逻辑ADC 在线调频程序用 100 ms 的定时器轮询 ADC读出来是一串 ASCII 数字intMainWindow::readAdc(){intfd::open(/dev/adc,O_RDONLY);if(fd0)return-1;charbuf[32];intlen::read(fd,buf,sizeof(buf)-1);::close(fd);if(len0)return-1;boolokfalse;intvQString(buf).trimmed().toInt(ok);returnok?v:-1;}拿到原始值后做死区滤波再线性映射到频率区间/* 死区与上次相差不到 40 就认为是抖动不更新 */staticintlastV-1;if(lastV0(vlastV-40)(vlastV40))return;lastVv;/* 线性映射 */doubleledFLED_FREQ_MIN(double)v/ADC_MAX*(LED_FREQ_MAX-LED_FREQ_MIN);intpwmFPWM_FREQ_MIN(int)((qint64)v*(PWM_FREQ_MAX-PWM_FREQ_MIN)/ADC_MAX);映射范围LED0.5~10 HzPWM100~10000 HzADC_MAX 4095。LED 的闪烁用另一个定时器实现亮灭 f 次/秒所以每半个周期翻转一次电平周期是1000/(2f)毫秒。七、踩坑记录这部分最值钱坑 1改了频率蜂鸣器音调纹丝不动现象调用PWM_SET_FREQ写进去新频率蜂鸣器还是原来的音调。原因Exynos4412 的定时器只有在 TCON 的 update 位由 0 变 1 的跳变时才会把 TCNTB1 装载到计数器里。只写 TCNTB1 不触发 update新值不会生效。解法每次改频率都要走一遍OFF → SET_FREQ → ON/* 先 PWM_OFF 清掉 update 位再写频率最后 PWM_ON 重新启动 */ioctl(m_pwmFd,PWM_OFF);ioctl(m_pwmFd,PWM_SET_FREQ,f);ioctl(m_pwmFd,PWM_ON);这也是前面 PWM 驱动流程里为什么要有PWM_OFF这一条命令的原因。坑 2ADC 读数一直抖频率跟着乱跳旋钮不动的时候ADC 读数也会在 ±20 左右抖动导致频率被反复改写。加了 ±40 的死区滤波之后就稳了——牺牲一点点分辨率换来稳定的输出。坑 3程序一启动蜂鸣器就响原因pwm-driver的open会对定时器和 GPIO 做初始化等于一打开设备就开始输出。解法不要在构造函数里 open/dev/pwm改成用户点 Start 时才打开、点 Stop 时PWM_OFF再close。设备文件描述符平时保持 -1。坑 4LED 驱动一 open 就把灯点亮了驱动的open里给两个 GPIO 写了初值 1所以一打开设备两颗灯就亮。应用层打开后要立刻补一次熄灭保证上电初始状态一致m_ledFd::open(/dev/led_ctl,O_RDWR);ioctl(m_ledFd,IO_CMD_LED_1,0);ioctl(m_ledFd,IO_CMD_LED_2,0);坑 5小屏上控件被挤扁480×272 上按默认字体排完按钮会互相挤压。把全局字号设成 8 磅、主布局边距和间距设成 4 像素之后才排得下。QFont fthis-font();f.setPointSize(8);this-setFont(f);mainLayout-setMargin(4);mainLayout-setSpacing(4);八、实测效果下面的照片是实际运行时拍的界面参数配置、LED 按设定频率闪烁、用 ADC 旋钮在线调整、以及调节 ADC 让蜂鸣器发出不同频率的声音。九、怎么复现编译驱动进入leds和pwm目录用交叉工具链编译出ledsdev.ko和pwm-driver.ko拷到开发板insmod ledsdev.ko、insmod pwm-driver.kols /dev/led_ctl /dev/pwm确认节点存在编译 Qt 程序pwm_adjuster拷到板子上运行先点 LED 区的 Start 验证闪烁再点蜂鸣器区的 Start 验证发声最后转 ADC 旋钮验证在线调频。还可以往下做目前 PWM 占空比固定 50%可以开放占空比调节得到更丰富的波形LED 只用了 LED1 指示闪烁可以加多种闪烁模式界面上还可以加一条频率曲线让调节过程更直观。如果你也在做 4412 的课程设计建议先按驱动能单独跑通用测试程序控制灯和蜂鸣器→ 再上 Qt 界面的顺序推进比一上来就写界面要省很多时间。有遇到ioctl不生效或者 ADC 读数异常的情况欢迎在评论区说下你的现象我可以帮忙看下。
返回列表