基于STM32单片机(HAL库)和淘晶驰串口屏的USART串口通信(CLion+CubeMX)及2024 TI杯电赛初赛赛后总结

一、写在前面

前段时间算是完赛了24年的电赛初赛,在正赛及赛前一个月的集训里一直负责串口屏交互的代码编写和UI设计,稍微积攒了一些经验,想着把自己踩过的坑写出来给后人们排雷。在集训初期摸索这个串口屏的打开方式时,笔者发现虽然网络上有大量基于淘晶驰串口屏的使用例程,但实操中可能存在环境不同、库不匹配、版本不同、硬件连线出错等问题,对于为了打电赛从零开始学习串口屏通信的同学来说很搞心态,一些小bug会卡很久很久(我就在这个破玩意儿上卡了一周多)。因此写这篇博客也算是尽一些绵薄之力把容易踩的坑排除一下。

二、软硬件配置及环境

硬件方面:

(1) STM32G474RETx单片机系统板

(2) 淘晶驰X5系列10.1存串口屏(电容触摸版)

上位机UI工程烧录工具采用官方USB转TTL套件

软件方面:

(1) CLion 2024.1.4 (截至20240729最新版)

(2) CubeMX 6.12.0 (截至20240729最新版)

(3) USART HMI(于淘晶驰官方资料中心下载)淘晶驰资料中心 — 淘晶驰资料中心 1.1.0-2024-08-06 17:45:50 文档 (tjc1688.com)icon-default.png?t=N7T8http://wiki.tjc1688.com/

三、上位机工程

关于上位机工程的具体制作方法在淘晶驰官方网站(即上方链接)中有详解,堪称保姆级,这也是采用淘晶驰串口屏的好处之一,UI设计确实很好上手。此处仅对上位机工程中与串口通信相关的控件进行解释。

首先解释一下淘晶驰串口屏的通信协议。

对于从串口屏发出数据到单片机,官方在上位机程序中给出了prints和printh两种函数。笔者所写例程中均使用prints函数发送定长为2的字符串,并规定第一个字符为操作位,第二个字符为具体操作值,这样在代码中也比较便于处理。下图是官方prints函数说明页面的部分截取,其中lenth部分写0则代表发送整个完整的字符串。不过由于代码是处理定长数据的,为了防止溢出,建议仍对lenth写2,即发送2字节。注意所发送数据需要带“ ”号,否则会被当作数值类型发送。

对于从单片机回传数据至串口屏,数据的结束符为“0XFF 0XFF 0XFF”三个字节。

现给出笔者设计的UI各控件的返回值如下:

(1)【“操作位+操作值” 型返回值】

· 数字键盘,返回值 “N + 数字(字符型)”

如,按键b6的功能为数字键盘“1”,则按下后返回字符串“N1”

· “调节”按钮,返回值 “O + 数字(字符型)”

如,按键b0的功能为切换至对t0所示值进行调节,按下后返回字符串“O0”。此处建议按钮名、文本框名和返回值中的操作值相对应(b0->t0->O0),便于代码中进行操作。

· 步进按钮,返回值 “S + 数字(字符型)”

为适应本次电赛C题要求,特增加步进控件。按钮b8步进,返回“S0”;按钮b9为步退,返回“S1”。

(2)【纯字符型返回值】

· 退格键“CC”,确认键“EE”,重置键“RR”

· “CW”模式,“AM”模式

此控件也是为了适配电赛C题添加的。

配置好上位机工程后下载烧录到串口屏上即可,只要返回值格式正确,单片机就可以正确接收和处理串口屏发送的数据。UI部分可以自行设计,建议统一配色以获得更好的视觉效果。

注,在Program.s页中,笔者将波特率设置为115200;为保证串口通信正常,后续在CubeMX中也应按照115200设置串口波特率。

四、代码部分

首先自行安装CLion和CubeMX,并配置好联合开发环境,网络上教程很多,此处不赘述。

(一)CubeMX配置

串口屏和单片机的相互通信只占用两个IO口,只需要开启一个USART即可。本例程开启USART1。

· USART1

Mode选择Asynchronous,波特率配置115200,开启中断NVIC,其他默认

· SYS

Debug选Serial Wire,其他默认

· RCC

· CLOCK CONFIGURATION

· PROJECT MANAGER

Toolchain/IDE记得勾选STM32CubeIDE

配置完成后即可GENERATE CODE

(二)CLion代码编写

为了便于复用,主函数main.c里请尽量精简代码块,而将功能更多地写在外部.c文件中。本例程创建了一个HMI.h和一个HMI.c,需要复用时直接将两文件复制到相应CLion工程的Inc和Src文件夹中即可。

此处直接给出完整代码,注释写的还算清楚。

HMI.h

#ifndef __HMI_H__
#define __HMI_H__
typedef struct{
    char name[100]; //项目名
    char unit[100]; //项目所用单位
    uint32_t buff; //缓冲区,存当前最后一个输入值
    uint32_t output; //输出区,存实际输出值,函数中引入的变量请代入此值
    uint32_t min; //最小输入范围
    uint32_t max; //最大输入范围
    uint32_t step; //步长
} ITEM; //对要输入输出的项目进行定义
extern ITEM items[50]; //至多50个项目
void HMIInit();
/*----------------------------------------------------*/
void HMISends(const char *buf1);
void HMISendb();
/*----------------------------------------------------*/
void HMIKeyboard(uint32_t opt, char *rx_byte);
uint32_t HMIChangeIndex(char *rx_byte);


#endif

此处声明了一个结构体ITEM,extern该结构体,当在其他文件中调用该,h文件时即可直接对结构体数组items[ ]的元素进行操作。

该结构体的元素即为你要计数的项目,配置项目的名称和单位并在代码中对上位机的各文本值进行修改,可便于直接在C代码中更改文本,避免频繁烧录上位机程序。

HMI.c

首先给出完整代码:

#include <math.h>
#include "main.h"
#include "usart.h"
#include "stdio.h"
#include "HMI.h"
ITEM items[50];

void HMIInit() //初始化items
{
    for (int i = 0; i < 50; i++) {
        items[i].name[0] = '\0'; //使用前记得重定义名称
        items[i].unit[0] = '\0'; //使用前记得重定义单位
        items[i].buff = 0;
        items[i].output = 0;
        items[i].min = 0;
        items[i].max = 65535; //使用前记得修改上下限
        items[i].step = 0;
    }
}
/*----------------------------------------------------*/
void HMISends(const char *buf1)
{
    if (buf1 == NULL) {
        return; // 如果传入的字符串为空,直接返回
    }
    while (*buf1) // 检查字符串是否结束
    {
        HAL_UART_Transmit(&huart1, (uint8_t *)buf1, 1, HAL_MAX_DELAY); // 发送一个字节
        buf1++; // 移动到下一个字符
    }
}
void HMISendb()
{
    uint8_t k=0xff;
    for (uint8_t i = 0; i < 3; i++) {
        HAL_UART_Transmit(&huart1, &k, 1, HAL_MAX_DELAY); // 发送一个字节
    }
}
/*----------------------------------------------------*/
void HMIKeyboard(uint32_t opt, char *rx_byte)
{
    int RxBuff;
    RxBuff = items[opt].buff; //将当前操作对象在文本框中的显示值赋给缓冲区
    switch (rx_byte[0]) //判断标识位
    {
        case 'N': //NUMBER //按下数字键,在文本框中更新当前待输出值,直到最大值max
            RxBuff *= 10; //乘十进位
            RxBuff += (rx_byte[1] - 48); //个位加上所键入的数
            /*限制取值上限*/
            if (RxBuff > items[opt].max) {
                items[opt].buff = items[opt].max;
            }
            else {
                items[opt].buff = RxBuff;
            }
            break;
        case 'S': //STEP
            if (rx_byte[1] == '0') { //STEP++
                RxBuff += items[opt].step;
                if (RxBuff > items[opt].max) {
                    items[opt].buff = items[opt].max;
                }
                else {
                    items[opt].buff = RxBuff;
                }
            }
            else if (rx_byte[1] == '1') { //STEP--
                RxBuff -= items[opt].step;
                if (RxBuff < 0) {
                    items[opt].buff = 0;
                }
                else {
                    items[opt].buff = RxBuff;
                }
            }
            break;
        case 'E': //ENSURE //按下确认键,判断是否合法输出
            int output_ready = 0; //输出标准
            for (int j = 0; j < 6; j++) { //遍历,判断是否合法输出
                if (items[j].buff < items[j].min) {
                    output_ready = 0;
                    break;
                }
                else output_ready = 1;
            }
            if (output_ready == 1) { //合法输出
                for (int j = 0; j < 6; j++) {
                    items[j].output = items[j].buff;
                }
                char msg[100];
                sprintf(msg,"func0.t6.txt=\"已输出\"");
                HMISends(msg);
                HMISendb();
            }
            else { //非法输出
                char msg[100];
                sprintf(msg,"func0.t6.txt=\"存在非法输入!请检查待输出值的范围!\"");
                HMISends(msg);
                HMISendb();
            }
            break;
        case 'C': //CANCEL //按下退格键,在文本框中更新当前待输出值,直到最小值min
            RxBuff /= 10; //除以十退位
            /*下限为0,使键入待输出值更加方便*/
            items[opt].buff = RxBuff;
            break;
        case 'R': //RESET //按下重置键,将所有buff还原至min,但不清除output
            char msg[100];
            sprintf(msg,"func0.t6.txt=\"已重置为各值下限\"");
            HMISends(msg);
            HMISendb();
            for (int j = 0; j < 6; j++) {
                items[j].buff = items[j].min;
            }
            break;
        default:
            break;
    }

}
uint32_t HMIChangeIndex(char *rx_byte)
{
    uint32_t index;
    index = rx_byte[1] - 48;
    return index; //返回当前opt值
}


各部分说明如下。

HMISends和HMISendb两函数在调用时需要一起使用:HMISends将你要发送的字符串拆分成一个个单独的字符发送,HMISendb发送终止符0xff0xff0xff。

/*--------------------使用例 BEGIN---------------------*/
    char msg[100];
    sprintf(msg,"Hello World");
    HMISends(msg);
    HMISendb();
/*--------------------使用例 END-----------------------*/

HMIKeyboard键盘部分的逻辑部分不复杂,自行阅读理解即可,大部分代码是在用上下限框定输入输出值的范围。读者可以自行添加控件,配置相应的返回值和功能,注意返回值是长度为2的字符串。

HMIChangeIndex用于在main.c中改变opt的值,opt对应于每个items[ ]的下标,也对应于上位机程序中相应控件的名称。

main.c

由于主函数自动生成部分过于冗长,且不同版本CubeMX自动生成代码部分存在差异,可能存在误导,此处仅将USER CODE部分按顺序给出,请读者自行在相应位置添加代码。

添加Include:

/* USER CODE BEGIN Includes */
#include "HMI.h"
#include "stdio.h"
#include "string.h"
/* USER CODE END Includes */

添加Private Variables:

/* USER CODE BEGIN PV */

uint32_t opt = 0; //代表当前进行的操作,opt即为手动配置项目时各项目的数组下标,且与上位机程序中的'O+option'回传相对应
char rx_byte[2]; //接收到的字符 //单次接收2字符,为标识位+操作位

uint32_t CW_ON = 1, AM_ON = 0; //切换CW或AM

/* USER CODE END PV */

在USER CODE 0添加中断回调函数:

/* USER CODE BEGIN 0 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (rx_byte[0] == 'C' && rx_byte[1] == 'W') { //切换至CW
        CW_ON = 1;
        AM_ON = 0;
        char msg[100];
        sprintf(msg,"func0.t6.txt=\"已切换至 CW\"");
        HMISends(msg);
        HMISendb();
    }
    else if (rx_byte[0] == 'A' && rx_byte[1] == 'M') { //切换至AM
        CW_ON = 0;
        AM_ON = 1;
        char msg[100];
        sprintf(msg,"func0.t6.txt=\"已切换至 AM\"");
        HMISends(msg);
        HMISendb();
    }
    else if (rx_byte[0] == 'O') { //切换所调节的值
        opt = HMIChangeIndex(rx_byte);
        char msg[100];
        sprintf(msg,"func0.t6.txt=\"正在调节:%s\"",
                items[opt].name
        );
        HMISends(msg);
        HMISendb();
        strcpy(msg, '\0');
        sprintf(msg,"func0.b8.txt=\"+%d%s\"",
                items[opt].step,
                items[opt].unit
        );
        HMISends(msg);
        HMISendb();
        strcpy(msg, '\0');
        sprintf(msg,"func0.b9.txt=\"-%d%s\"",
                items[opt].step,
                items[opt].unit
        );
        HMISends(msg);
        HMISendb();
    }
    else HMIKeyboard(opt, rx_byte);

    /*------------手动配置输出 BEGIN----------------*/
    for (int i = 0; i < 6; i++) {
        char msg[100];
        sprintf(msg,"func0.t%d.txt=\"%s:%d%s\"",
                i,
                items[i].name,
                items[i].buff,
                items[i].unit
        );
        HMISends(msg);
        HMISendb();
    }
    /*------------手动配置输出 END------------------*/

    HAL_UART_Receive_IT(&huart1, (uint8_t *)rx_byte, 2);
}
/* USER CODE END 0 */

代码的前半部分是赛时仓促添加的,较为冗杂,读者可以尝试将切换CW和AM以及切换opt等操作也封装入HMI.c中。

再次注意,该中断回调只接受长度为2的字符串。

在USER CODE 1自定义各计数项目的参数:

    /* USER CODE BEGIN 1 */
    /*-----------手动配置各项目的名称、单位、上下限 BEGIN--------------*/
    HMIInit(); //items初始化

    strcat(items[0].name, "直达信号载波频率\0");
    strcat(items[0].unit, "MHz\0");
    items[0].min = 30;
    items[0].max = 40;
    items[0].step = 1;

    strcat(items[1].name, "载波幅度有效值\0");
    strcat(items[1].unit, "mV\0");
    items[1].min = 100;
    items[1].max = 1000;
    items[1].step = 100;

    strcat(items[2].name, "AM调制度\0");
    strcat(items[2].unit, "%\0");
    items[2].min = 30;
    items[2].max = 90;
    items[2].step = 10;

    strcat(items[3].name, "多径信号初始相位\0");
    strcat(items[3].unit, "°\0");
    items[3].min = 0;
    items[3].max = 180;
    items[3].step = 30;

    strcat(items[4].name, "多径信号幅度衰减\0");
    strcat(items[4].unit, "dB\0");
    items[4].min = 0;
    items[4].max = 20;
    items[4].step = 2;

    strcat(items[5].name, "多径信号时延\0");
    strcat(items[5].unit, "ns\0");
    items[5].min = 50;
    items[5].max = 200;
    items[5].step = 30;
    /*-----------手动配置各项目的名称、单位、上下限 END----------------*/
    /* USER CODE END 1 */

在USER CODE 2写入初始输出,并开启中断回调:

    /* USER CODE BEGIN 2 */

    /* 初始输出 */
    for (int i = 0; i < 6; i++) {
        char msg[100];
        sprintf(msg,"func0.t%d.txt=\"%s:%d%s\"",
                i,
                items[i].name,
                items[i].buff,
                items[i].unit
        );
        HMISends(msg);
        HMISendb();
    }
    char msg[100];
    sprintf(msg,"func0.t7.txt=\"CW\"");
    HMISends(msg);
    HMISendb();
    /* 进入中断回调 */
    HAL_UART_Receive_IT(&huart1, (uint8_t *)rx_byte, 2);

    /* USER CODE END 2 */

以上为所需的全部代码。

五、硬件连接

对于连线,最重要的就是保持单片机和串口屏的共地,且注意RX->TX,TX->RX。同时建议使用外部电源供电,因为常规的电脑USB接口只提供至多500mA的电流,对于点亮七寸以上的串口屏是比较吃力的,若通过单片机供电则容易电流过大烧坏单片机。淘晶驰官方基于F103单片机系统板给出了连线示意图如下。

笔者的实物连线图如下:

配置CubeMX时,我们设置的USART1,PC4为TX,PC5为RX,查数据手册获得端口位置如下:

共地时选取单片机上任意两个GND连线即可,共地设备较多时可以考虑用面包板。

六、最终效果展示

开机后,首先点击单片机上的RST按键,屏幕显示数值如下图所示:

点击“直达信号载波频率”的“调节”按钮,键入“35”,效果如下图所示:

点击“重置”,效果如下图所示:

点击“输出”,效果如下图所示:

七、一些小tips

(1)供电不稳或共地不良可能出现屏幕无法点亮的情况。用原装4pin连接线延伸屏幕接口,再用双公头杜邦线连接4pin连接线对应引脚和单片机母头GND,以获得更好的供电效果;

(2)屏幕初始亮度过亮也可能导致黑屏。在上位机程序的Program.s文件中将亮度变量“dim=100”修改为“dim=20”,亲测该亮度在室内不刺眼且显示清晰。还可以单独设置一个settings页面,用滑块控件修改亮度,具体参考淘晶驰官方滑块控件示例。

(3)若串口屏无法收到单片机回传的数据,再次检查:

        · 单片机和串口屏是否共地

        · 单片机的RX和TX是否正确对应连接串口屏的TX和RX

(4)新版CubeMX和CLion的联合开发环境中,若将外部.h文件复制到Inc文件夹并添加到CMake,会在CMakeLists.txt对LINKER_SCRIPT的配置添加一段代码,导致编译出错。修改为以.ld项结尾,如下:

八、一点小心得

关于电赛信号类题目的建议。

· 赛前各种模块该设计的设计,该测试的测试,该打板的打板。信号的输入输出端口同时预留SMA和插针以备不时之需;电源接口最好统一用接线端子或JST,不要直接用插针,一方面便于一眼识别电源接口不易接错,另一方面接线更稳不容易虚接;选取合适的芯片,使用前认真阅读数据手册,在规定的频段和偏置电压内使用芯片,有效防止芯片烧毁和自激。

· 供电可自己设计一个简单的电源模块,包括常用的电源端口如【-3V3, GND, +3V3】,【GND, +3V3】,【-5V, GND, +5V】,【GND, +5V】。若允许使用移动电源,可将模块外部接口设计为TYPE-C的PD输入,用支持PD协议的充电宝供电,相比直接使用实验台上的直流电流源,其波纹会更小。

· 设计简单的通用运放模块(0603或0805电阻电容本即可覆盖大部分所需值,同时预备好0Ω电阻和100MΩ电阻)可快速焊接积分器、微分器、加法器、有源滤波器等。

· 打板时预留LED电源指示灯可以快速检验模块是否成功上电,极大方便了断路、虚焊的检查。

· 各模块初次上电使用时,用实验台的直流电流源供电,记录各状态下的电流,调试时心里有个数。(一般情况下实验台的稳压还是挺稳的,如果突然出现明显的压降+电流猛增,99.99%是芯片烧了

从学期末开始到七月底的电赛正赛期间确实是学了不少东西,尤其是提升动手实操能力,课内学习的仅仅是理论基础;电赛不是模电的DLC,而是模拟电路、数字电路、单片机基础等课程的综合体。希望大家能在电赛的集训和正赛过程中真真正正的收获知识,提升专业素养,这才是电赛的意义所在。最后,借用学长每次集训ppt的结尾:

(附两张我队最终成品剪影)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值