嵌入式入门之路(持续更新)

    科技2024-07-12  84

    文章目录

    前言一、需要实现的功能二、关注方向1.使用串口完成PC和设备通讯2.如何自定义串口协议3.如何通过串口管理外部设备4.IAP5.上位机开发6.FreeModbus协议7.移植性8.Web端 三、实现过程及原理3.1串口通信3.2 上位机编程3.3 IAP 四、遇到的问题以及解决方法4.1>串口通信部分4.2>上位机部分 日志总结


    前言

    本文用于记载我嵌入式学习的一个过程。 记于2020.10.7


    提示:以下是本篇文章正文内容

    一、需要实现的功能

    (1)通过串口完成PC和下位机的通讯 (2)串口使用自定义协议,包含起始位,地址,数据,结束位,crc校验 (3)通过串口能够管理外部设备,如复位,蜂鸣器,设置DA输出,获取AD值,并在介面显示 (4)支持串口在线升级,存储使用外部FLASH,下载支持断点重传,版本检验以及完整性检验,可自定义检验方法,如累加和,crc等。 (5)上位机开发支持自定义串口协议的软件,使用常见的能够实现图形界面的语言和框架,如C#,C++,python (6)进阶1.0:支持上述自定义协议替换为FreeModbus协议,使用宏切换 (7)进阶2.0:将协议和上层应用部分与驱动独立开来设计,利用宏使应用和协议部分支持多平台的移植 (8)进阶3.0:使用Web端,能通过浏览器实现上述功能。


    二、关注方向

    1.使用串口完成PC和设备通讯

    如果要用串口实现上位机下位机的通讯,有哪些步骤? 又或者我希望完成PC和安卓/单片机的通信,如何实现?

    2.如何自定义串口协议

    自定义串口协议有哪些步骤呢?会涉及到哪些学科的知识? 接收方如何识别起始位,数据,结束位,以及crc校验? crc校验的具体过程以及实现机理?

    3.如何通过串口管理外部设备

    这句话的意思是什么?通过上位机来管理外部设备吗? 如果我想要通过电脑来控制下位机的设备(蜂鸣器,LED等),我需要在电脑端 设计一个介面,这个介面用什么来设计?要用到什么软件?怎么设计更人性化?

    4.IAP

    如何实现串口自动升级,这里有哪些步骤,实现的具体过程是什么? 存储使用外部FLASH是指什么,我需要外接FLASH吗,我需要通过软硬件去配置吗,以及如何配置? 断点重传是什么意思?实现过程是什么?技术上如何实现? 版本校验和完整性校验是基于什么样的原理?实现的过程是什么?如何自定义一个 校验方法?这个过程需要我掌握什么? 累加和,crc是否属于校验方法的一部分呢?以及如何实现这两个功能?

    5.上位机开发

    如何开发一款支持自定义协议的操作介面?框架一词经常听到,它对于整个项目来说意味着什么?如何用一个简单的例子来理解呢?

    6.FreeModbus协议

    FreeModbus协议是什么?通讯过程是什么?和IIC,SPI,CAN通讯协议有无关联?或者它们的区别是什么?

    7.移植性

    如何将协议和上层应用部分与驱动独立开来设计?网上有没有类似的视频?

    8.Web端

    如何通过web端实现上述功能,是否需要写一个网页?web如何与下位机通讯,原来的通讯协议是否需要修改?

    三、实现过程及原理

    3.1串口通信

    目前,我通过串口助手完成PC与STM32F103的双向通信 只有当两者波特率一致时,才能正常收发数据,否则会导致乱码。

    1>使能时钟,在对STM32的初始化程序进行编程时,第一步永远是时钟初始化; 不同功能的初始化意味着我们需要初始化不同的时钟,本文我们需要对USART进行初始化完成串口通信,而在STM32中,USART的RX引脚和TX引脚与GPIO端口复用,所以我们需要对GPIO和USART的时钟进行使能;缺一不可!

    GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; //串口参数的配置,比如波特率,CRC校验等

    2>上一点也说到了,我们需要对GPIO和USART进行时钟的使能,这也意味着我们需要初始化两者的结构体,对结构体的成员变量进行赋值;由第一点可知,我们在配置GPIO端口工作模式的时候,不再是简单的推挽输出,需要复用;

    TX引脚用于发送数据,也就是输出模式; GPIO_InitStructure.GPIO_Pin=USART1_TX;//定义发送引脚 GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;//复用PA9的引脚功能,设置为复用输出 GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;//定义输出速度 而RX用于接收数据,即输入模式; GPIO_InitStructure.GPIO_Pin=USART1_RX;//定义接收引脚 GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;//定义输出速度

    最后,我们需要配置串口的参数,一般来说,我们将参数配置为96-N-8-1即可; 波特率设置为9600,无校验位,8位数据位,1位停止位

    USART_InitStructure.USART_BaudRate = baud;//波特率设置 USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式 USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位 USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //双工

    3>配置串口参数后,我们需要将USART1使能,并打开接收中断,使之能够接收上位机发送的数据。当USART1接收到上位机发送的数据后,会产生中断,我们需要在中断部分将接收的数据用一个变量保存起来,用以功能的扩展;这也意味着我们需要配置NVIC,NVIC的配置同常规的一致,只需要将抢占优先级和响应优先级设置为最低即可。这里不再展示NVIC配置的代码。

    4>上述配置完成后,我简单的叙述一下中断部分的编程思路:当数据被接受并被存储到移位接收寄存器中后,USART_IT_RXNE置1,所以我们需要根据它的状态进行判断,之后,我们设置一个变量将接收的数据存储,如果我们接下来需要通过TX引脚将接收的数据返回给上位机,并显示;我们需要调用已经配置好的函数

    void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)

    将数据发送出去,但一般来说中断部分只做数据的接收,不进行数据处理,所以真正运用时需要注意一下了

    3.2 上位机编程

    上位机编程主要是界面的编写,以串口助手为模板;实现这个过程可以通过QT或者C#,这两者我都完成了串口助手的编写,但这里仅就C#进行展开。

    串口助手的功能主要是通过串口接收下位机的数据,整个的设计思路如下: 1>设置上位机读取串口的一些基本信息,比如COM口,波特率,数据位、结束位的个数等等。 2>我们需要将读取/发送的数据显示在特定位置,比如我们设置一个专用的显示框 (Text),以及一个专用的发送框(Text)。 3>最后,我们需要实现数据发送的功能,即:将发送框的内容通过串口发送至下位机。这个过程不论是在QT,还是C#,都有已经封装好的函数可以直接调用。

    第一步:窗口的设置 这里我以一个通用的助手为模板,进行设计: 第二步:串口参数的设置 需要设置的参数一般有5个:端口号,波特率,数据位,停止位,校验。这里需要提供一系列选项,这里我是直接在Combobox属性框的Items中直接添加的,当然也可以通过代码来添加。 再将所有的参数选择完毕后,通过Open Port按键,实现串口数据的赋值,毕竟Combobox的数值与串口配置的参数还是有区别的,这里可以通过一个if语句来实现,也可以通过*switch…case…*来实现。

    private void btn_OpenPort_Click(object sender, EventArgs e) { try { //将可能产生异常的代码放置在try块中 //根据当前串口属性来判断是否打开 if (serialPort1.IsOpen) { //串口已经处于打开状态 serialPort1.Close(); //关闭串口 btn_OpenPort.Text = "打开串口"; btn_OpenPort.BackColor = Color.ForestGreen; cbx_PortName.Enabled = true; cbx_DateBits.Enabled = true; cbx_BaudRate.Enabled = true; cbx_Parity.Enabled = true; cbx_StopBits.Enabled = true; textBox_receive.Text = ""; //清空接收区 textBox_send.Text = ""; //清空发送区 } else { //串口已经处于关闭状态,则设置好串口属性后打开 cbx_PortName.Enabled = false; cbx_DateBits.Enabled = false; cbx_BaudRate.Enabled = false; cbx_Parity.Enabled = false; cbx_StopBits.Enabled = false; serialPort1.PortName = cbx_PortName.Text; serialPort1.BaudRate = Convert.ToInt32(cbx_BaudRate.Text); serialPort1.DataBits = Convert.ToInt16(cbx_DateBits.Text); if (cbx_Parity.Text.Equals("None")) serialPort1.Parity = System.IO.Ports.Parity.None; else if (cbx_Parity.Text.Equals("Odd")) serialPort1.Parity = System.IO.Ports.Parity.Odd; else if (cbx_Parity.Text.Equals("Even")) serialPort1.Parity = System.IO.Ports.Parity.Even; else if (cbx_Parity.Text.Equals("Mark")) serialPort1.Parity = System.IO.Ports.Parity.Mark; else if (cbx_Parity.Text.Equals("Space")) serialPort1.Parity = System.IO.Ports.Parity.Space; if (cbx_StopBits.Text.Equals("1")) serialPort1.StopBits = System.IO.Ports.StopBits.One; else if (cbx_StopBits.Text.Equals("1.5")) serialPort1.StopBits = System.IO.Ports.StopBits.OnePointFive; else if (cbx_StopBits.Text.Equals("2")) serialPort1.StopBits = System.IO.Ports.StopBits.Two; serialPort1.Open(); //打开串口 btn_OpenPort.Text = "关闭串口"; btn_OpenPort.BackColor = Color.Firebrick; } } catch (Exception ex) { //捕获可能发生的异常并进行处理 //捕获到异常,创建一个新的对象,之前的不可以再用 serialPort1 = new System.IO.Ports.SerialPort(); //刷新COM口选项 cbx_PortName.Items.Clear(); cbx_PortName.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames()); //响铃并显示异常给用户 System.Media.SystemSounds.Beep.Play(); btn_OpenPort.Text = "打开串口"; btn_OpenPort.BackColor = Color.ForestGreen; MessageBox.Show(ex.Message); cbx_PortName.Enabled = true; cbx_DateBits.Enabled = true; cbx_BaudRate.Enabled = true; cbx_Parity.Enabled = true; cbx_StopBits.Enabled = true; } }

    第三步:串口数据的读取 这里的数据读取需要先为串口注册一个Receive事件: 以及代码部分的实现:

    private void SerialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e) { try { //因为要访问UI资源,所以需要使用invoke方式同步ui this.Invoke((EventHandler)(delegate { textBox_receive.AppendText(serialPort1.ReadExisting()); } ) ); } catch (Exception ex) { //响铃并显示异常给用户 System.Media.SystemSounds.Beep.Play(); MessageBox.Show(ex.Message); } } 到这里,可以通过单片机向上位机发送数据,来检测接收部分的实现效果。

    第四步:数据的发送

    数据在发送之前,需要先检测端口是否打开,只有确认打开了串口,才可以进行数据的发送。 private void btn_send_Click(object sender, EventArgs e) { try { //首先判断串口是否开启 if (serialPort1.IsOpen) { //串口处于开启状态,将发送区文本发送 serialPort1.Write(textBox_send.Text); } } catch (Exception ex) { //捕获到异常,创建一个新的对象,之前的不可以再用 serialPort1 = new System.IO.Ports.SerialPort(); //刷新COM口选项 cbx_PortName.Items.Clear(); cbx_PortName.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames()); //响铃并显示异常给用户 System.Media.SystemSounds.Beep.Play(); btn_OpenPort.Text = "打开串口"; btn_OpenPort.BackColor = Color.ForestGreen; MessageBox.Show(ex.Message); cbx_PortName.Enabled = true; cbx_DateBits.Enabled = true; cbx_BaudRate.Enabled = true; cbx_Parity.Enabled = true; cbx_StopBits.Enabled = true; } }

    第五步:效果展示

    3.3 IAP

    四、遇到的问题以及解决方法

    4.1>串口通信部分

    在单纯的进行串口通信过程中(不涉及管理外设),会出现一种情况:单个字母能够正常的通讯,但当我发送一串字符时,会丢失部分数据;

    通过上网查询得知,中断部分的一条语句导致程序运行过慢,导致上位机数据发完了,而下位机只接受到前面部分的数据;

    void USART1_IRQHandler(void) { u8 r; if(USART_GetITStatus(USART1,USART_IT_RXNE)!=RESET) { r = USART_ReceiveData(USART1); DateControl(r);//通过上位机控制下位机的外设 USART_SendData(USART1,r); //while(USART_GetFlagStatus(USART1,USART_FLAG_TC) != SET); } USART_ClearFlag(USART1,USART_FLAG_TC); }

    注释掉的语句就是导致上述问题的“元凶”; 理解如下: 串口有移位寄存器,我们在发送之前判断一下发送移位寄存器是否满,如果未满则将数据发送即可。而不是发送状态寄存器的状态位;如果发送移位寄存器满了,硬件会自动帮我们把数据传递给发送寄存器,然后传递出去;但本条语句,就是在判断发送寄存器的状态,这也意味着,我们的时间浪费在“把移位寄存器的值放到发送寄存器+发送出去”两步上,事实上这不是我们需要关心的事情。

    解决方法就像我代码所示,将那条语句注释掉,或者直接删掉就可以了

    4.2>上位机部分

    这里我在上位机调试的时候出现了一个问题:数据发送后,单片机并没有给我返回数值,这个问题并不是因为上位机编程的错误;如果有遇到我这样的问题,解决的方法如下:

    尝试重新向单片机烧写程序,通过这个方式,我达到了上述图片的效果。实现了简单的数据通信。

    日志

    2020.12.13: 实现了一个简单的串口助手,虽然功能也很少,但是未来的一段时间,我也会继续丰富相关的功能,但通过实现数据的通信,这能够帮助我们实现很多功能; 计划:在接下来半个月(12.13-12.28)的事件,了解IAP的基本原理以及具体的功能实现。

    总结

    革命尚未成功,同志仍需努力!

    Processed: 0.023, SQL: 8