嵌入式学习笔记(综合提高篇 第一章) -- 利用串口点亮/关闭LED灯

1      前言  

  从踏入嵌入式行业到现在已经过去了4年多,参与开发过的产品不少,有交换机、光端机以及光纤收发器,停车场出入缴费系统,二维码扫码枪,智能指纹锁以及数字IC芯片开发等; 涉及产品中中既有STM和Nuvoton这类通用芯片,也有Nordic-52832,Nordic-52810,易兆微这种专用的蓝牙芯片,还包含用于WIFI设备的ESP32芯片,以及专业的指纹/二维码安全芯片,当然也参与过基于ARM9内核的Linux的嵌入式服务器开发和维护,更详细的参与了异步双核MCU的验证工作和库开发,虽然它们内核和性能参数各异,甚至开发工具也大不相同,但是经过工作积累,就会发现这些MCU的开发都有比较清晰的流程,难度往往并不在本身的驱动调试开发部分。协议/安全/稳定性,图像/GUI/视频处理,性能/电源管理/低功耗,行业相关需求,这些知识在产品开发中才是最重要的。

  在有了C语言基础,熟悉常见的开发工具如keil,Iar或者arm-gcc,了解芯片的基本I/O和寄存器配置后,底层模块的驱动在整个产品开发流程中其实是占比最少的一部分,而RTOS选用/移植,任务管理/通讯,复杂协议如(TCP/IP, USB, BLE)等的移植运用,功能逻辑实现,软/硬件功能调试,以及后期功能测试才是项目的主要部分,而这些往往是初期很难了解,也不知道如何去掌握的知识,只有从丰富的项目开发经历中才能总结出来。在我入门的过程中,也是把重点放在模块的学习上,从GPIO,Uart,中断等一步步开了解芯片的构造,可当我学习到DCMI,ETH,LTDC时,一方面模块复杂,另一方面需要配合大量的软件部分实现,往往在没有成果反馈的情况下就失去了学习钻研的兴趣,最后不得不半途而废;在入职嵌入式行业, 处理过多个产品项目后才逐渐有些明白,这些模块的复杂程度即使是熟练工,也需要花费一定时间才能去熟练掌握,基于模块的应用开发更可能需要按星期算的时间,在没有详细目的驱使的情况下,如果把重点放在去掌握整个模块的基本功能上,即枯燥也很困难(熟练掌握是重要的,但应用才是产品开发的核心,先学会用,在长时间的运用后熟练也是学习的一种方法)。在这份文档中将摒弃以模块为核心的初期积累方式,将以应用为核心,产品开发的思路为向导,先规划产品需求,在由简入难,讲诉如何将想法化作嵌入式产品的过程,以及其中遇到的困难和解决办法。

  嵌入式内部也根据行业有很多方向,如通讯行业,涉及有交换机,路由器,视频光端机,要掌握各种通讯协议,如TCP/IP, 环网协议等; 安全行业, 涉及有监控系统,支付扫码枪,指纹锁,要掌握视频,图像相关的处理知识,也要了解国密算法如SM2, SM3, HASH等,还有电源行业,涉及有充电器,适配器,就要了解BC1.2,高通QC快充以及PD协议等,其它行业没参与过,不太了解,但深入各行业之后有点很清楚,对行业的理解才是决定自己发展的最大限制,在学习和提高的过程中,可以选择更全面,但在工作中,一定要选择自己最合适的方向深入耕耘。

1.1    资料准备

  工欲善其事,必先利其器。从事嵌入式开发的学习,首先选择合适的开发芯片和开发工具当然是十分重要的,如果已经有开发板或者芯片模组,那么直接使用即可,没有的话建议选择意法半导体的STM系列芯片,原因有以下几点。

    • 产品量大,比起飞思卡尔/TI的芯片,网络上使用的人更多,遇到问题网络上也更容易找到解决问题的办法。
    • ST作为比较早进入中国的公司,对于资料方面中文化更全面(嵌入式开发英文很重要,但中文更适合入门)。
    • 国内单片机方面的开发板也是以ST的居多,如比较出名的正点原子,野火等,也更好选择,新人购买也建议选择这些开发板,功能应用齐全。

  当然目前因为手里只有一块之前购买ST的STM32F7-Discovery,使用STMF746G芯片,因此就以此为核心进行后续应用的开发和总结,另外因为个人熟悉程度和常用开发,选择MDK5作为开发工具。做好准备后,就开始第一个课题:利用串口点亮/关闭LED灯,具体要求如下:

    • 上位机带软件界面,有两个按键分别控制开灯/关灯
    • 下位机可根据按键控制LED,有一定的扩展性(后续支持其它功能?)

  资料/设备(本文档所在的资料文件内有附加工具/文档):

    • 开发板STM32F7-Discovery
    • USB转串口工具
    • 笔记本一台
    • USB供电线,用于打印
    • 文档若干(stm32f7-discovery原理图, STM32F7x参考手册(中文), STM32F7数据手册)

  准备好上述工具,就可以开始需求的正式的功能开发了,首先要确定开发需求涉及到的知识点,上位机软件因为有窗口和串口,那么使用C#/或者Python+PyGUI都可以,个人擅长C#,因此选择C#写上位机软件; 至于下位机因为要采集串口数据,并控制LED,因此涉及到USART-输入/GPIO两个模块,考虑到会用到打印调试,因此USART-printf也最好实现,另外考虑到实时性和架构的需求,USART使用中断模式为佳,总结下整个开发就包含下面流程:

    • 下位机GPIO,USART,中断模块的驱动实现
    • 上位机软件开发
    • 上位机/下位机交互的规则,以及对实际硬件的操作

1.1     硬件驱动实现

  不过因为初期可以用串口工具模拟上位机软件,因此首先进行驱动实现和交互协议规则实现,驱动的实现在有一定单片机基础后并不困难,首先确认对应的硬件的实际接口,具体如下:

  LED -- GPIOI1

  USART1 TX -– PA9

  USART6 TX –- PC6, RX – PC7

  具体参考《stm32f7-discovery原理图》,详细如下

嵌入式学习笔记(综合提高篇 第一章) -- 利用串口点亮/关闭LED灯

嵌入式学习笔记(综合提高篇 第一章) -- 利用串口点亮/关闭LED灯

图 1 硬件原理图

  确定了硬件之后,就开始驱动的编写,这里可以简化总结下窍门,对于涉及硬件但不涉及复杂协议的接口,如USART,I2c或者SPI等,硬件接口的实现一般包含以下几部分:

  1. 使能模块对应RCC的时钟(包含对应GPIO模块和应用模块)
  2. 配置对应的硬件GPIO口,单纯GPIO这一步结束,接口则配置为相应的复用模式
  3. 配置硬件模块,使能
  4. 如果开启中断,需要配置相应的优先级,并实现中断函数。

  根据上述说明,LED的初始化函数如下(具体配置说明参考STM32F7x参考手册)

void  led_gpio_init(void)
{ //使能GPIOI的时钟
RCC->AHB1ENR |= <<; //配置引脚为输出
GPIOI->MODER &= ~(0x3<<);
GPIOI->MODER |= 0x1<<; //配置为推挽模式
GPIOI->OTYPER &= ~(0x1<<); //默认无上下拉
GPIOI->PUPDR &= ~(0x3<<);
}

  这里有一个知识点,引脚推挽/开漏,其中推挽表示内部有上拉/下拉电阻,能够对外部供电,开漏则只能输出0和高阻态 , 输出高电平需要外部上拉电阻(适合驱动大电流外设),本例中因为没有外部上拉电阻只能使用推挽模式。

USART的初始化函数分成两部分, USART相应引脚初始化

void  usart_gpio_init(void)
{ //使能GPIOC, GPIOA时钟
RCC->AHB1ENR |= ((<<)|(<<)); //PA9引脚配置
MODIFY_REG(GPIOA->MODER, 0x3<<, 0x2<<); //复用模式
MODIFY_REG(GPIOA->OTYPER, 0x1<<, ); //推挽输出
MODIFY_REG(GPIOA->PUPDR, 0x3<<, 0x1<<); //默认上拉
MODIFY_REG(GPIOA->OSPEEDR, 0x3<<, 0x3<<); //高速模式
MODIFY_REG(GPIOA->AFR[], 0xf<<, 0x7<<); //复用模式USART1 //PC6引脚配置(输出)
MODIFY_REG(GPIOC->MODER, 0x3<<, 0x2<<); //复用模式
MODIFY_REG(GPIOA->OTYPER, 0x1<<, ); //推挽输出
MODIFY_REG(GPIOC->PUPDR, 0x3<<, 0x1<<); //默认上拉
MODIFY_REG(GPIOC->OSPEEDR, 0x3<<, 0x3<<); //高速模式
MODIFY_REG(GPIOC->AFR[], 0xf<<, 0x8<<); //复用模式USART6
//PC7引脚配置(输入)
MODIFY_REG(GPIOC->MODER, 0x3<<, 0x2<<); //复用模式
MODIFY_REG(GPIOC->PUPDR, 0x3<<, <<); //无上拉下拉
MODIFY_REG(GPIOC->OSPEEDR, 0x3<<, 0x3<<); //高速模式
MODIFY_REG(GPIOC->AFR[], (uint32_t)0xf<<, (uint32_t)0x8<<); //复用模式USART6
}

这里需要注意的就是复用模式的选择,需要参考STM32F7数据手册第三章Table12 STM32F745xx and STM32F746xx alternate function mapping即可,例程参考如下图

嵌入式学习笔记(综合提高篇 第一章) -- 利用串口点亮/关闭LED灯

图 2 引脚重定义信息

USART模块初始化及中断函数则如下:

void  usart_module_init(void)
{ //使能USART1和USART6
RCC->APB2ENR |= ((<<)|(<<)); //USART1
//配置为1个起始位, 8个数据位, 1个停止位)
//16倍过采样, 禁止奇/偶校验, 不使用CTS/RTS
//只支持输出模式
//波特率 9600
USART1->CR1 = ;
USART1->CR2 = ;
USART1->BRR = Get_SystemFrequency(CLOCK_APB2)/; //根据模块时钟获得采样率
USART1->CR1 |= ((<<) | (<<)); //使能USART和USART发送
sendstring(USART1, "USART1 Start OK!\r\n", strlen("USART1 Start OK!\r\n"));
//USART6
//配置为1个起始位, 9个数据位, 1个停止位)
//16倍过采样, 支持偶校验,不使用CTS/RTS
//波特率 115200
//支持输入输出模式, 输入使用中断模式
USART6->CR1 = ((<<) | (<<) | (<<));
USART6->CR2 = ;
USART6->RQR |= (<<); //清除接收标志位
USART6->BRR = Get_SystemFrequency(CLOCK_APB2)/; //根据模块时钟获得采样率
USART6->CR1 |= ((<<) | (<<) | (<<)); //使能USART、USART发送、USART接收
sendstring(USART6, "USART6 Start OK!\r\n", strlen("USART6 Start OK!\r\n")); //配置中断寄存器, 开启中断
SCB->AIRCR = (VECTKEYSTAT | NVIC_PriorityGroup_4); NVIC_SetPriority(USART6_IRQn, );
NVIC_EnableIRQ(USART6_IRQn);
}

USART6中断函数和简单测试程序则如下:

void  USART6_IRQHandler(void)
{
char ch; //读取数据, 同时清除标志位
ch = USART6->RDR; printf("%c", ch);
}

  上述涉及的知识点有串口参数(数据位,起始位,停止位,奇偶校验,时钟频率/波特率),串口中断,中断向量表,中断函数,串口接收/发送,其实这些东西不理解照样可以写出成功运行的程序,不过理解之后才能更加系统的完善提高自身,当然这里就不在赘述,想详细了解也可以参考之前的学习笔记中关于USART部分及中断的说明,因为这些并不是功能开发的重点。完成了硬件驱动部分,在依靠简单的测试程序,实现简单的串口输入输出检测, 如图 3所示。

嵌入式学习笔记(综合提高篇 第一章) -- 利用串口点亮/关闭LED灯

图 3  USART输入输出测试

1.2     协议制定和实现

  完成硬件部分处理,下面就可以进行交互协议和驱动的实现了,事实上,如果仅点亮、关闭LED灯,设计上仅需要简单的定义0为熄灭,1为点亮就可以轻松实现,不过这只是从简单实现上考虑,而不是从一个成熟应用的角度考虑,对于涉及多设备的通讯,可靠性和可扩展性都是不可或缺的,那么使用自定义的私有协议就是比较认可的方式,现在主流的通讯方式是以指令命令行为核心的字符串控制协议,如蓝牙模块,wifi模块使用的AT指令,优点是支持直接的命令行操作,另一种则是以数据结构组合/解析为核心的二进制通讯协议,如常见的TCP/IP协议,USB协议等,这里考虑到后续的扩展性,以及纯C实现的难度(可扩展字符串解析在不使用正则的情况下使用C语言很复杂),我决定采用第二种方式,通讯分为请求帧/应答帧主从机模式。

请求帧格式

帧头(1byte)

设备号

(1byte)

帧类型

(1byte)

数据长度

(2byte)

数据

(0~1000byte)

校验

(2byte)

0x5a

0x00~0xFE

表示设备号

0xFF

表示该位无效

0x01 设备控制帧

0x02 参数修改帧

0x03 其它

0~1000

具体数据

CRC16校验码

应答帧格式

帧头(1byte)

设备号

(1byte)

响应状态/数据提交

(1byte)

数据长度

(2byte)

数据

(0~1000byte)

校验

(2byte)

0x5b

与本机设备一致

0x00 成功

0xff 消息提交

其它 失败

0~1000

具体数据

CRC16校验码

  其中帧头/校验用来保证数据的完整性,设备号用于保证当有多个下位机是能够正常的管理,帧类型主要考虑到后续功能的完善,如添加下载功能,能够进行区分,数据则就是直接对底层硬件的处理信息,这里暂时定义LED设备为00,UART设备01。对于点亮LED耗时可能很短,但如果实现将上位机下发数据通过串口打印,则需要比较多的时间,这里我们采用接收通过USART进行,处理则通过主程序完成(当然处理任务比较多时,使用RTOS也是建议的选择), 下面开始上述功能参考

嵌入式学习笔记(综合提高篇 第一章) -- 利用串口点亮/关闭LED灯

图 2‑4  串口协议接收/处理简单流程

  完成上述流程代码后,通过测试工具发送二进制数据5a 01 01 00 02 00 01 xx xx和5a 01 01 00 02 00 00 xx xx就可以分别点亮和关闭LED,并返回相应的响应,至于上位机因为和嵌入式部分关系并不大,因此这里不说明具体实现思路(其实和下位机很相似,不过需要些C#知识),仅提供代码和实现程序,当然这里还有些知识点并没有详细说,如CRC的软件和硬件实现,结构体的对齐机制,串口接收的错误处理机制等,因为本身资料很多,这里不在赘述,希望了解的可自行检索,另外这里大致协议处理中核心的函数的实现方式, 如串口中断数据接收函数。

void  USART6_IRQHandler(void)
{ char c;
static uint16_t repos = ;
static uint16_t total_len = ; //读取数据, 同时清除标志位
if((USART6->ISR & (<<)) != )
{
c = USART6->RDR;
if(USART_STATUS.reflag == )
{
if(repos == )
{
if(c == 0x5a) //起始位, 保证接收到一帧的开始
{
total_len = ;
memset(USART_RX_BUFF, , USART_BUFF_SIZE);
USART_RX_BUFF[repos++] = c;
}
}
else if(repos == ) //在当前长度下,可以获得携带数据的长度len
{
USART_RX_BUFF[repos++] = c;
total_len = (USART_RX_BUFF[]<<)+USART_RX_BUFF[]+;
}
else if(repos == total_len- || repos>=USART_BUFF_SIZE) //接收到完整帧,可能溢出
{
USART_RX_BUFF[repos++] = c;
USART_STATUS.reflag = ;
USART_STATUS.relen = repos;
repos = ;
}
else
USART_RX_BUFF[repos++] = c;
}
else
repos = ;
}
//printf("%c", ch);
}

另外具体代码较多,详见对应工程usart.c和UsartProtocol.c文件。实现了协议并通过测试,下面就可以配合上位机软件实现远程点亮LED,如图所示

嵌入式学习笔记(综合提高篇 第一章) -- 利用串口点亮/关闭LED灯

1.3     总结和知识点

  仔细体验下来就会发现对于协议处理部分占用的工作远远超过对驱动的处理,实现包含协议的制定,流程规划,协议实现,数据测试,最后在将结果反馈到实际的硬件模块上, 如点亮/关闭LED灯,这里可能就有个疑问了,为什么使用如此复杂的协议,而不直接使用00/01来管理硬件了,这里其实是十分重要的问题,也是入门时最难理解的问题,这里我个人根据工作经验来整理自己的看法。

  • 产品的可靠性,串口虽然是稳定成熟的接口,可在干扰或者硬件不稳定的情况下,仍然有出错的风险,对于点亮/关闭LED来说,可能只是指示的错误,影响并不大,但如果这些数据是安全操作开关,阀门的开启/关闭,控制门锁等,没有协议头和校验,出错影响的就是生命财产的安全。
  • 未来的可扩展性,这里我们只通过串口实现了关闭/打开LED灯,如果以后我们想通过它支持多设备,修改调试/本身串口的波特率,获取设备的版本信息,下载固件,下载图片,如果没有一套完整的约束,后续还是会出现管理问题。

  虽然本例是使用PC的串口与芯片通讯,事实上嵌入式开发中常见的是芯片与芯片之间的通讯,它们之间的通讯接口也不限于串口,更多的有I2C、SPI、CAN或USB等,虽然有各自的特点,但这套协议对于所有的接口是共通的,所以学会制定和实现自定义协议是嵌入式中十分重要的能力,也可以为后续学习复杂协议如TCP/IP, USB或BLE等打下基础。至此,涉及GPIO,USART,RCC,NVIC,CRC/HASH等模块的USART远程管理LED代码的程序就完成了,虽然开发还算顺利,但在实现过程中,回顾整理所学,还是让我受益匪浅,不枉花费如此时间。

  本章中我们了解了底层模块驱动实现的方案,另外也初步知晓了双机通讯的相关知识,阐述了协议的重要性以及实现的方法和思路,这些在一般项目中已经属于软件核心的一部分,值得亲自实践。下面先来看我曾经参加过的项目,其结构如所示

嵌入式学习笔记(综合提高篇 第一章) -- 利用串口点亮/关闭LED灯

图 5  停车场管理系统结构简图

  上述停车场管理系统结构中,和嵌入式开发相关的有:

  • LED标识装置 -- 用于显示当前剩余车辆,与基站通讯保持实时刷新
  • 超声波采集设备 -- 低功耗设备,覆盖所有停车位,负责检测车辆存在,并通过射频提交基站
  • wifi/RF基站 -- 整合车位信息,更新LED显示,提交服务器数据,并接收/处理/转发服务器管理数据

  虽然这些模块的通讯接口各异,有射频RFID,RS485以及wifi,但这些设备之间通讯都进行了十分严格的约束,有简单的自定义协议,也有复杂的通用网络协议TCP/IP,上述模块是由多个人员开发了,协议不仅仅作为通讯的约束,也保证了多过人员开发时的配合有效性,因此协议在大型的工程多机联合项目中基本上是不可或缺的,掌握协议开发也是嵌入式工程师的基础能力之一,值得深入学习。

  另外本章节涉及的部分细节知识点并没有详细阐述,如果想了解可根据下面知识点自行检索。

  • RCC,GPIO,USART,CRC,NVIC寄存器配置,硬件操作
  • 串口参数:起始位,数据位,停止位,奇偶校验
  • 中断向量表,中断函数的实现
  • 硬件/软件CRC校验
  • 数据结构对齐机制

本项目开发相关资料和代码实现见附件:

链接:https://pan.baidu.com/s/1pXkKG3y8ItXWqXLC4ODuaw

提取码:kzol

上一篇:【Cocos2d-X开发学习笔记】第05期:渲染框架之布景层类(CCLayer)的使用


下一篇:Arduino控制LED灯(开关控制)