嵌入式Linux学习(一)蓝牙控制炫彩灯
基于正点原子IMX6ULL开发板蓝牙驱动炫彩灯+QT显示控制
目录
一、前言
很开心可以开始第一次博客之旅,从这篇文章开始我将会记录每次关于嵌入式学习的项目记录和日记,以此来不断加强自己的专业技术,首先非常感谢正点原子所有教程视频、开源资料带给我的帮助,不管是各种硬件板子、软件代码,都让我更加对嵌入式Linux着迷,我很多项目都是参考网上很多大佬开源项目进行学习,也让我走上了嵌入式学习之路!也希望以后能与其他电子爱好者更多的进行技术分享交流,若哪里做得不好不对的地方需要改进的,请大家多多指教!
这个项目是基于恩智浦芯片i.MX6ULL控制的嵌入式Linux系统,而且采用QT界面设计来进行显示控制,可以利用触摸屏对炫彩灯进行不同占空比的输出,根据HC05蓝牙模块与手机进行通信,手机也可以发送不同字符串来控制炫彩灯的各种灯效。至于嵌入式Linux的各种如Ubuntu安装、QT、Uboot、根文件系统、触摸屏等其他驱动移植这里暂时就不写了,这个文章主要专注于记录功能的实现,QT源代码也会进行开源分享,将会放在本文最下方!!!
二、PWM控制炫彩灯
1、设备树添加引脚和节点内容
这里使用厂家内核自带的PWM驱动,自己就不需要编写了,记得在Linux内核配置界面打开使能就好。炫彩灯为红蓝绿三种颜色,所以需要三个引脚进行PWM输出。只需要在设备树文件的iomuxc节点下添加引脚信息,然后再对这三个引脚各自添加三个节点设置时钟就可以了。
首先需要屏蔽掉其他复用的IO,避免重复使用,板子有许多PWM通道,这里使用PWM3通道对应复用引脚之一的GPIO_IO04引脚,PWM6通道对应复用引脚之一的ENET1_TX_EN,PWM8通道对应复用引脚之一的ENET1_RX_ER。具体通道还有哪些复用引脚等可以使用请参考芯片手册!
pinctrl_pwm3: pwm3grp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO04__PWM3_OUT 0x110b0
>;
};
pinctrl_pwm_6: pwm6grp {
fsl,pins = <
MX6UL_PAD_ENET1_TX_EN__PWM6_OUT 0x10b0
>;
};
pinctrl_pwm_8: pwm8grp {
fsl,pins = <
MX6UL_PAD_ENET1_RX_ER__PWM8_OUT 0x10b0
>;
};
节点这里每个引脚需要对应使用的PWM通道并且设置时钟和okay状态,然后编译下设备树文件,成功后把文件放到板子上重新启动。
&pwm3 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_pwm3>;
status = "okay";
};
&pwm6 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_pwm_6>;
clocks = <&clks IMX6UL_CLK_PWM6>,
<&clks IMX6UL_CLK_PWM6>;
clock-names = "ipg", "per";
status = "okay";
};
&pwm8 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_pwm_8>;
clocks = <&clks IMX6UL_CLK_PWM8>,
<&clks IMX6UL_CLK_PWM8>;
clock-names = "ipg", "per";
status = "okay";
};
2、PWM驱动测试
开发板启动完后进入终端根目录/sys/class/pwm/中,这里存放pwm驱动的所有通道,目录显示的pwmchip0 ~ 7分别对应实际的pwm1 ~ 6,这里先设置红灯的pwm6通道对应pwmchip5,利用sys接口方式进行pwm驱动。
首先输入指令“echo 0 >/sys/class/pwm/pwchip5/export”,export导出pwm0的接口,然后在对应的pwm通道目录pwmchip5中,找到pwm0进入就可以设置参数了。
设置参数测试有3个步骤,(1)先输入指令“echo 1 >/sys/class/pwm/pwmchip5/pwm0/enable”,enable使能可以驱动起来,想关闭就把1改为0灯就灭了。
(2)然后输入“echo 50000 >/sys/class/pwm/pwmchip5/pwm0/period”,设置周期50000。
(3)跟着输入“echo 10000 >/sys/class/pwm/pwmchip5/pwm0/duty_cycle”,占空比为10000,然后就可以看到灯是否亮起来了,如果不清晰可以不断增加占空比看看。另外2个pwm也是同理进行导出驱动。
3、错误总结
一开始错误示范:这里设置节点只有简单设置通道,并没有处理其他东西,导致输入echo 1 enable使能时整个终端卡住,灯没有任何反应。
正确做法:后面发现是时钟的问题,因为PWM计数器无时钟驱动 → 无法生成波形,内核访问未初始化的模块 → 触发超时阻塞(卡死),所以应该还要设置时钟:
三、开发板与电脑进行蓝牙串口通信
1、蓝牙模块HC05参数设置
用电脑串口助手XCOM进行设置,先按住HC05重置按键然后在上电,这样进入AT命令响应模式对模块进行一些参数设置了,LED灯由快闪变成双慢闪。这里列出需要用到的AT指令:
| 指令名 | 响应回复 | 含义 |
|---|---|---|
| AT | OK | 测试指令 |
| AT+RESET | OK | 模块复位 |
| AT+UART=115200,0,0 | OK | 设置串口参数 |
| AT+UART? | +UART:115200,0,0 OK | 显示串口参数 |
大部分名字密码可以默认(可以自己修改),默认使用的波特率为38400,需要修改下,这里设置关键的串口参数115200,0,0,第一个为对应匹配波特率115200,第二个为停止位0(0指1),第三个校验位0为None。
2、手机蓝牙通过HC05与电脑串口通信测试
修改好参数就可以重新上电跟手机配对了,选择正确串口和波特率115200,打开串口按下发送按键发送信息“我是电脑!”。
首先蓝牙需要配对HC05成功后,然后再使用蓝牙串口助手APP,按下连接成功后发送“我是手机!”,双端成功实现通信!!
3、开发板通过HC05与电脑串口通信测试
这里先移植minicom串口助手到开发板中(电脑软件MobaXterm终端显示),输入指令-v查看版本号成功则移植成功,输入指令-s运行minicom测试。
进来后先进入“Serial port setup”进行串口设置。
这个为串口设置界面,按键盘“A”设置串口设备为ttymxc2(对应开发板串口接口名称),按“E”设置比特为115200对应蓝牙模块波特率,按“F”设置“”Hardware Flow Control”设置为No,其他默认就好,然后按enter确认退出。
然后按下“Ctrl-A Z”进入信息设置界面后,按下键盘字母“E”打开回显功能,这样接收的信息才会显示在终端上面。开发板(电脑终端界面显示)通过蓝牙串口与手机进行通信成功(终端直接输入就可以直接发送过去了):
手机蓝牙APP界面成功显示发送“enen”和接收到“233”(但是有几率出现空格“ ”不知道为什么,有时还会出现问号乱码):
4、错误总结
无论是手机端还是开发板端若出现乱码,可能是字符集不匹配造成的,需要对应的字符集一样才可以,因为后面QT代码用的串口发送接收数据都是以UTF-8的字符集,所以手机APP设置同样的UTF-8,若还是出现乱码可以设置同样的GBK字符集试试。
四、QT界面功能总设计与测试
1、炫彩灯控制
炫彩灯有红绿蓝三种颜色,它们各种占空比输出的亮度大小可以实现不同种颜色,各自设计三个滑条QT控件类"Horizontal Scroll Bar",滑条数值最大,占空比越大,亮度越大,一种颜色最大值为255,可以设计三种颜色不同比例来组合颜色,如红255,绿255,蓝为255,全设置最大值时,炫彩灯显现纯白色。
ui布局放好之后,创建三个滑条槽函数,当滑动滑条时进入槽函数,文本框rednum显示数值,传送滑动值到redctrl函数(此为设置占空比函数),另外两种颜色函数同样设计。
//红灯占空比、滑条和显示框使用函数
void Widget::on_red_valueChanged(int value)
{
ui->red->setValue(value);
ui->rednum->setText(QString::number(value));
redctrl(value);
}
//绿灯滑条和显示框使用函数
void Widget::on_green_valueChanged(int value)
{
ui->green->setValue(value);
ui->greennum->setText(QString::number(value));
greenctrl(value);
}
//蓝灯滑条和显示框使用函数
void Widget::on_blue_valueChanged(int value)
{
ui->blue->setValue(value);
ui->bluenum->setText(QString::number(value));
bluectrl(value);
}
这里用sys接口的方式去控制PWM,所以先需要导出接口,这段代码直接放到最前面默认运行程序就运行:
QString cmd_config1 = "echo 0 > /sys/class/pwm/pwmchip2/export";
QByteArray cmdby_off1 = cmd_config1.toLatin1();
char* charCmd_config1 = cmdby_off1.data();
system(charCmd_config1);
QString cmd_config2 = "echo 0 > /sys/class/pwm/pwmchip5/export";
QByteArray cmdby_off2 = cmd_config2.toLatin1();
char* charCmd_config2 = cmdby_off2.data();
system(charCmd_config2);
QString cmd_config3 = "echo 0 > /sys/class/pwm/pwmchip7/export";
QByteArray cmdby_off3 = cmd_config3.toLatin1();
char* charCmd_config3 = cmdby_off3.data();
system(charCmd_config3);
当数值进入改变控制亮度大小的函数(改变占空比)时,因为占空比最大只有100%,所以数值(滑条最大数值为255)先进行转换对应比例,然后设置频率、占空比、使能。
//红色占空比改变函数
void Widget::redctrl(int duty)
{
/*QString path = PWM_LED_PATH; */
/* 由于system函数只能用char类型,所以进行了转换 */
/* config */
/* period */
QString cmd_period = "echo 1000000 > /sys/class/pwm/pwmchip5/pwm0/period";
QByteArray cmdby_period = cmd_period.toLatin1();
char* charCmd_period = cmdby_period.data();
system(charCmd_period);
int change = duty;
change = (change*1000000)/255;
qDebug() << "红色: " << change;
/* duty cycle */
QString cmd_duty = "echo "+ QString::number(change, 10) +" > /sys/class/pwm/pwmchip5/pwm0/duty_cycle";
QByteArray cmdby_duty = cmd_duty.toLatin1();
char* charCmd_duty = cmdby_duty.data();
system(charCmd_duty);
/* enable */
QString cmd_enable = "echo 1 > /sys/class/pwm/pwmchip5/pwm0/enable";
QByteArray cmdby_enable = cmd_enable.toLatin1();
char* charCmd_enable = cmdby_enable.data();
system(charCmd_enable);
}
//绿色占空比改变函数
void Widget::greenctrl(int duty)
{
/*QString path = PWM_LED_PATH; */
/* 由于system函数只能用char类型,所以进行了转换 */
/* config */
/* period */
QString cmd_period = "echo 1000000 > /sys/class/pwm/pwmchip7/pwm0/period";
QByteArray cmdby_period = cmd_period.toLatin1();
char* charCmd_period = cmdby_period.data();
system(charCmd_period);
int change = duty;
change = (change*1000000)/255;
qDebug() << "绿色: " << change;
/* duty cycle */
QString cmd_duty = "echo "+ QString::number(change, 10) +" > /sys/class/pwm/pwmchip7/pwm0/duty_cycle";
QByteArray cmdby_duty = cmd_duty.toLatin1();
char* charCmd_duty = cmdby_duty.data();
system(charCmd_duty);
/* enable */
QString cmd_enable = "echo 1 > /sys/class/pwm/pwmchip7/pwm0/enable";
QByteArray cmdby_enable = cmd_enable.toLatin1();
char* charCmd_enable = cmdby_enable.data();
system(charCmd_enable);
}
//蓝色占空比改变函数
void Widget::bluectrl(int duty)
{
/*QString path = PWM_LED_PATH; */
/* 由于system函数只能用char类型,所以进行了转换 */
/* config */
/* period */
QString cmd_period = "echo 1000000 > /sys/class/pwm/pwmchip2/pwm0/period";
QByteArray cmdby_period = cmd_period.toLatin1();
char* charCmd_period = cmdby_period.data();
system(charCmd_period);
int change = duty;
change = (change*1000000)/255;
qDebug() << "蓝色: " << change;
/* duty cycle */
QString cmd_duty = "echo "+ QString::number(change, 10) +" > /sys/class/pwm/pwmchip2/pwm0/duty_cycle";
QByteArray cmdby_duty = cmd_duty.toLatin1();
char* charCmd_duty = cmdby_duty.data();
system(charCmd_duty);
/* enable */
QString cmd_enable = "echo 1 > /sys/class/pwm/pwmchip2/pwm0/enable";
QByteArray cmdby_enable = cmd_enable.toLatin1();
char* charCmd_enable = cmdby_enable.data();
system(charCmd_enable);
}
2、串口通信
串口界面需要下拉控件ComboBox,文本框TextBrowser,按键PushButton,发送文本框TextEdit并且双击设置预发送的字符串(本来需要写个虚拟键盘的,但是工作量有亿点点大,这里偷懒嘿嘿就发送这句话算了),按照如图布局:
双击下拉框进行设置波特率:
双击下拉框进行设置停止位:
双击下拉框进行设置数据位:
双击下拉框进行设置校验位:
按下“打开串口”这个按键转到槽函数(bool类型),当检测到按键被按下的时候,串口类serialPort就匹配设置串口需要的一些配置名字、波特率、停止位、数据位,但是校验位的枚举不是数值,都是为字符串所以用switch来设置,然后继续设置流控,再加个若打开串口失败的弹窗。
//打开串口按键函数
void Widget::on_pushButton_clicked(bool checked)
{
if (checked) {
//设置要打开的串口名字
serialPort->setPortName(ui->comboBox->currentText());
//设置波特率
serialPort->setBaudRate(ui->comboBox_2->currentText().toInt());
//设置停止位
serialPort->setStopBits(QSerialPort::StopBits(ui->comboBox_3->currentText().toInt()));
//设置数据位
serialPort->setDataBits(QSerialPort::DataBits(ui->comboBox_4->currentText().toInt()));
//设置校验位,枚举不是数值,是字符串所以用switch
switch (ui->comboBox_5->currentIndex()) {
case 0:
serialPort->setParity(QSerialPort::NoParity);
break;
case 1:
serialPort->setParity(QSerialPort::EvenParity);
break;
case 2:
serialPort->setParity(QSerialPort::OddParity);
break;
case 3:
serialPort->setParity(QSerialPort::SpaceParity);
break;
case 4:
serialPort->setParity(QSerialPort::MarkParity);
break;
default:
break;
}
//设置流控
serialPort->setFlowControl(QSerialPort::NoFlowControl);
if (!serialPort->open(QIODevice::ReadWrite)) {
QMessageBox::about(this, "错误", "串口打开失败可能被占用");//弹出弹窗表示提醒
return;
}
//打开串口时按键不可用
ui->comboBox->setEnabled(false);
ui->comboBox_2->setEnabled(false);
ui->comboBox_3->setEnabled(false);
ui->comboBox_4->setEnabled(false);
ui->comboBox_5->setEnabled(false);
//串口操作按键显示“关闭串口”
ui->pushButton->setText("关闭串口");
}else {
//关闭串口
serialPort->close();
//关闭串口后按键可继续选用
ui->comboBox->setEnabled(true);
ui->comboBox_2->setEnabled(true);
ui->comboBox_3->setEnabled(true);
ui->comboBox_4->setEnabled(true);
ui->comboBox_5->setEnabled(true);
//串口操作按键显示“打开串口”
ui->pushButton->setText("打开串口");
}
}
扫描本机串口这段代码直接放前面,当程序一运行时就直接开始扫描串口设备,按下“打开串口”按键成功匹配后,再把串口设备名称显示到下拉框中:
serialPort = new QSerialPort(this);
//扫描本机的串口,并且添加到下拉框里
foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) {
ui->comboBox->addItem(info.portName());
}
//读取数据
connect(serialPort, SIGNAL(readyRead()), this, SLOT(readData()));
按下“发送”按键进入槽函数,把文本框的预输入的字符串用匹配的Utf8类型发送,清空文本框字符串直接就clear完事了。
//发送数据函数
void Widget::on_pushButton_2_clicked()
{
//发送数据
serialPort->write(ui->textEdit->toPlainText().toUtf8());
/*
QString text = ui->textEdit->toPlainText();
QTextCodec *gbkCodec = QTextCodec::codecForName("GBK");
if (gbkCodec) {
QByteArray gbkData = gbkCodec->fromUnicode(text);
serialPort->write(gbkData);
}*/
}
//清空发送数据
void Widget::on_pushButton_3_clicked()
{
ui->textEdit->clear();
}
当接收到数据时,把数据不断append追加到文本框中,这样就不会实时更新,数据会留有记录,再根据手机发送的字符串(1,2,3简简单单)匹配来控制灯效。
//接收数据函数
void Widget::readData()
{
//接收数据显示到文本框中
QByteArray change = serialPort->readAll();//只调用一次,就清空缓冲区数据
//显示数据在文本框中,append为追加,所有数据存留在文本框中
ui->textBrowser->append(change);
QString shuju = QString(change).trimmed();
qDebug() << "数据为:" << shuju;
//蓝牙接收字符串调灯
if (shuju == "1"){
on_pushButton_4_clicked(true);//收到指令进入LED按键槽函数
qDebug() << "接收到1了啊!!" ;
}
if (shuju == "2"){
on_pushButton_5_clicked(true);
qDebug() << "接收到2了啊!!" ;
}
if (shuju == "3"){
on_pushButton_6_clicked(true);
qDebug() << "接收到3了啊!!" ;
}
}
3、蓝牙控制灯效
最终界面如图:
另外在右下角添加了3种灯效,直接按下按键炫彩灯就可以变成一种灯效,当蓝牙接收到不同字符串时,将会进入对应的灯效函数,里面的灯效函数传送已设定好的数值改变占空比。
//霓虹灯灯效
void Widget::on_pushButton_4_clicked(bool checked)
{
if (checked){
/*
redctrl(255);
greenctrl(0);
bluectrl(255);
*/
this->on_red_valueChanged(255);
this->on_green_valueChanged(0);
this->on_blue_valueChanged(255);
ui->pushButton_4->setChecked(true);//确保按键为已按下状态
ui->textBrowser->append("正在显示霓虹灯灯效!");
}
}
//海洋灯效
void Widget::on_pushButton_5_clicked(bool checked)
{
if (checked){
/*
redctrl(0);
greenctrl(255);
bluectrl(255);
*/
this->on_red_valueChanged(0);
this->on_green_valueChanged(255);
this->on_blue_valueChanged(255);
ui->pushButton_5->setChecked(true);
ui->textBrowser->append("正在显示海洋灯效!");
}
}
//深紫主题灯效
void Widget::on_pushButton_6_clicked(bool checked)
{
if (checked){
/*
redctrl(204);
greenctrl(0);
bluectrl(204);
*/
this->on_red_valueChanged(204);
this->on_green_valueChanged(0);
this->on_blue_valueChanged(204);
ui->pushButton_6->setChecked(true);
ui->textBrowser->append("正在显示深紫主题灯效!");
}
}
4、错误总结
在测试蓝牙控制炫彩灯时,手机发送字符串后,终端和文本框都毫无反应,什么都不显示,而且灯也没有变化。
一开始的错误示范:然后发现这里serialPort->readAll()是属于调用一次数据后就完全清除缓冲区的数据了,第二次再调用相当于储存了个寂寞,本来想先显示文本框后再储存数据到change中这个方法是不对的。
正确做法:所以将这3行改为先调用一次直接储存数据到change中(这样只用一次serialPort->readAll()了),后利用change再显示文本框,转换数据为QString类型判断发来的字符串等操作。
//接收数据显示到文本框中
QByteArray change = serialPort->readAll();//只调用一次,就清空缓冲区数据
//显示数据在文本框中,append为追加,所有数据存留在文本框中
ui->textBrowser->append(change);
QString shuju = QString(change).trimmed();
五、总结与演示
QT代码构建编译好把可执行文件下载到开发板上,然后在MobaXterm终端上执行。1和3的灯效占空比数值相差不大,可能肉眼看不出来效果,重点是可以控制灯!!所以懒得改了,想弄其他颜色可以单独对灯效按键槽函数改变占空比数值(也是滑块的数值)!!!
功能演示如下:
蓝牙控制炫彩灯演示视频
这个小项目很早就搞定了,虽然很简单,但是是这里的第一次开始,写文章的时间感觉比做项目还要久哎。。希望以后能继续成长,继续学习更多电子技术,与更多大佬爱好者交流分享更多好项目好文章!!!
QT源代码和手机电脑相关APP都放git上面了,链接: link
更多推荐


所有评论(0)