• [技术干货] 采用ENC28J60以太网模块移植UIP、LWIP协议完成上云过程
    一、前言目前联网物联网云平台的都是采用ESP8266、BC20、BC26、SIM800C 等这些WIFI、NBIOT、GSM模块居多,本篇文章利用ENC28J60移植协议栈栈完成,联网操作。二、ENC28J60要进行以太网通信需要用到ENC28J60 以太网控制器和 uIP 1.0 以太网协议栈。下面将介绍这两个部分。3.1  ENC28J60 简介 3.1.1 ENC28J60芯片介绍ENC28J60 是带有行业标准串行外设接口(Serial Peripheral Interface,SPI)的独立以太网 控制器。它可作为任何配备有 SPI 的控制器的以太网接口。ENC28J60 符合 IEEE 802.3 的全部规范,采用了一系列包过滤机制以对传入数据包进行限制。 它还提供了一个内部 DMA 模块, 以实现快速数据吞吐和硬件支持的 IP 校验和计算。 与主控制器的通信通过两个中断引脚和 SPI 实现,数据传输速率高达 10 Mb/s。两个专用的引脚用于连接 LED,进行网络活动状态指示。ENC28J60 总共只有 28 脚,提供 QFN/TF。  ENC28J60 的主要特点如下: 兼容3 协议的以太网控制器 集成 MAC 和 10 BASE-T 物理层 支持全双工和半双工模式 数据冲突时可编程自动重发 SPI 接口速度可达 10Mbps 8K 数据接收和发送双端口 RAM 提供快速数据移动的内部 DMA 控制器 可配置的接收和发送缓冲区大小 两个可编程 LED 输出 带7个中断源的两个中断引脚 TTL 电平输入 提供多种封装:SOIC/SSOP/SPDIP/QFN 等。ENC28J60 的典型应用电路如下图:ENC28J60 由七个主要功能模块组成: 1) SPI 接口,充当主控制器和 ENC28J60 之间通信通道。 2) 控制寄存器,用于控制和监视 ENC28J60。 3) 双端口 RAM 缓冲器,用于接收和发送数据包。 4) 判优器,当 DMA、发送和接收模块发出请求时对 RAM 缓冲器的访问进行控制。 5) 总线接口,对通过 SPI 接收的数据和命令进行解析。 6) MAC(Medium Access Control)模块,实现符合 IEEE 802.3 标准的 MAC 逻辑。 7) PHY(物理层)模块,对双绞线上的模拟数据进行编码和译码。  ENC28J60 还包括其他支持模块,诸如振荡器、片内稳压器、电平变换器(提供可以接受 5V 电压的 I/O 引脚)和系统控制逻辑。ENC28J60 的功能框图如下图所示:ENC28J60实物图: 3.1.2 ENC28J60以太网模块介绍ENC28J60 网络模块采用 ENC28J60 作为主芯片,单芯片即可实现以太网接入, 利用该模块,基本上只要是个单片机,就可以实现以太网连接。模块实物图如下: 模块的主要引脚功能:其中 GND 和 V3.3 用于给模块供电,MISO/MOSI/SCK 用于 SPI 通信,CS 是片选信号,INT 为中断输出引脚,RST 为模块复位信号。3.2 嵌入式以太网协议栈简介uIP是一个简单好用的嵌入式网络协议栈,易于移植且消耗的内存空间较少,非常适合学习和使用。可以肯定的说uIP是嵌入式以太网学习的好起点,但不一定是终点。uIP的功能远不如LwIP强大,但两者并没有孰优孰劣之分,uIP和LwIP的作者同为Adam Dunkels,LwIP开发较早uIP开发较晚,uIP经过这几年的发展从IPV4迁移到IPV6,最终可以适用于无线传感网络。总的来说,uIP是一个很好的起点,学好uIP可以迁移到LwIP,也可以迁移到uIPV6。3.3 UIP 简介uIP 由瑞典计算机科学学院(网络嵌入式系统小组)的Adam Dunkels 开发。其源代码由C 语 言编写,并完全公开,uIP 的最新版本是 1.0 版本。 uIP 协议栈去掉了完整的 TCP/IP 中不常用的功能,简化了通讯流程,但保留了网络通信 必须使用的协议,设计重点放在了 IP/TCP/ICMP/UDP/ARP 这些网络层和传输层协议上,保证 了其代码的通用性和结构的稳定性。官网:https://github.com/adamdunkels/uip 由于 uIP 协议栈专门为嵌入式系统而设计,因此还具有如下优越功能:1) 代码非常少,其协议栈代码不到 6K,很方便阅读和移植。 2) 占用的内存数非常少,RAM 占用仅几百字节。 3) 其硬件处理层、协议栈层和应用层共用一个全局缓存区,不存在数据的拷贝,且发送 和接收都是依靠这个缓存区,极大的节省空间和时间。4) 支持多个主动连接和被动连接并发。 5) 其源代码中提供一套实例程序:web 服务器,web 客户端,电子邮件发送程序(SMTP 客 户端),Telnet 服务器, DNS 主机名解析程序等。通用性强,移植起来基本不用修改就可以通过。 6) 对数据的处理采用轮循机制,不需要操作系统的支持。 由于 uIP 对资源的需求少和移植容易,大部分的 8 位微控制器都使用过uIP 协议栈, 而且很多的著名的嵌入式产品和项目(如卫星,Cisco 路由器,无线传感器网络)中都在使用 uIP 协议栈。 uIP 相当于一个代码库,通过一系列的函数实现与底层硬件和高层应用程序的通讯,对于 整个系统来说它内部的协议组是透明的,从而增加了协议的通用性。uIP 协议栈与系统底层和高层应用之间的关系如图:从上图可以看出,uIP 协议栈主要提供 2 个函数供系统底层调用:uip_input 和 uip_periodic。 另外和应用程序联系主要是通过 UIP_APPCALL 函数。 当网卡驱动收到一个输入包时,将放入全局缓冲区uip_buf 中,包的大小由全局变量uip_len 约束。同时将调用 uip_input()函数,这个函数将会根据包首部的协议处理这个包和需要时调用 应用程序。当 uip_input()返回时,一个输出包同样放在全局缓冲区 uip_buf 里,大小赋给 uip_len。 如果 uip_len 是 0,则说明没有包要发送。否则调用底层系统的发包函数将包发送到网络上。 uIP 周期计时是用于驱动所有的 uIP 内部时钟事件。当周期计时激发,每一个 TCP 连接都 会调用 uIP 函数 uip_periodic()。类似于 uip_input()函数。uip_periodic()函数返回时,输出的 IP 包 要放到 uip_buf 中,供底层系统查询 uip_len 的大小发送。 由于使用 TCP/IP 的应用场景很多,因此应用程序作为单独的模块由用户实现。uIP 协议 栈提供一系列接口函数供用户程序调用,其中大部分函数是作为 C 的宏命令实现的,主要是为 了速度、代码大小、效率和堆栈的使用。用户需要将应用层入口程序作为接口提供给uIP 协议 栈, 并将这个函数定义为宏UIP_APPCALL()。这样,uIP 在接受到底层传来的数据包后,在需要送到上层应用程序处理的地方,调用 UIP_APPCALL( )。在不用修改协议栈的情况下可以适配不同的应用程序。 uIP 协议栈给我们提供了很多接口函数,这些函数在 uip.h 中定义,为了减少函数调用造成的额外支出,大部分接口函数以宏命令实现的。uIP 提供的接口函数有:1.初始化 uIP 协议栈:uip_init()2.处理输入包:uip_input() 3.处理周期计时事件:uip_periodic() 4.开始监听端口:uip_listen() 5.连接到远程主机:uip_connect() 6.接收到连接请求:uip_connected() 7.主动关闭连接:uip_close()8.连接被关闭:uip_closed() 9.发出去的数据被应答:uip_acked() 10.在当前连接发送数据:uip_send() 11.在当前连接上收到新的数据:uip_newdata() 12.告诉对方要停止连接:uip_stop()13.连接被意外终止:uip_aborted()3.4 UIP移植说明Uip源码分布如图:移植第一步:实现在 unix/tapdev.c 里面的三个函数。首先是 tapdev_init 函数,该函数用于 初始化网卡(也就是我们的 ENC28J60),通过这个函数实现网卡初始化。其次,是 tapdev_read 函数,该函数用于从网卡读取一包数据,将读到的数据存放在 uip_buf 里面,数据长度返回给 uip_len。最后,是 tapdev_send 函数,该函数用于向网卡发送一包数据,将全局缓存区 uip_buf 里面的数据发送出去(长度为 uip_len)。其实这三个函数就是实现最底层的网卡操作。 第二步:因为uIP协议栈需要使用时钟,为TCP和ARP的定时器服务,因此我们需要STM32 提供一个定时器做时钟,提供 10ms 计时(假设 clock-arch.h 里面的 CLOCK_CONF_SECOND 为 100),通过 clock-arch.c 里面的 clock_time 函数返回给 uIP 使用。 第三步:配置 uip-conf.h 里面的宏定义选项。主要用于设置 TCP 最大连接数、TCP 监听端口数、CPU 大小端模式等,这个大家根据自己需要配置即可。 通过以上 3 步的修改,我们基本上就完成了 uIP 的移植。在使用uIP 的时候,一般通过如下顺序: 1)实现接口函数(回调函数)UIP_APPCALL。 该函数是我们使用 uIP 最关键的部分,它是 uIP 和应用程序的接口,我们必须根据自己的需要,在该函数做各种处理,而做这些处理的触发条件,就是前面提到的 uIP 提供的 那些接口函数,如 uip_newdata、uip_acked、uip_closed 等等。另外,如果是 UDP,那么还需要实现 UIP_UDP_APPCALL 回调函数。 2)调用tapdev_init函数,先初始化网卡。 此步先初始化网卡,配置 MAC 地址,为 uIP 和网络通信做好准备。 3)调用 uip_init 函数,初始化 uIP 协议栈。 此步主要用于 uip 自身的初始化,我们直接调用就是。4) 设置 IP 地址、网关以及掩码这个和电脑上网差不多,只不过我们这里是通过 uip_ipaddr、uip_sethostaddr、 uip_setdraddr 和 uip_setnetmask 等函数实现。 5) 设置监听端口 uIP 根据你设定的不同监听端口,实现不同的服务,比如我们实现 Web Server 就监听 80 端口(浏览器默认的端口是 80 端口),凡是发现 80 端口的数据,都通过 Web Server 的 APPCALL 函数处理。根据自己的需要设置不同的监听端口。不过 uIP 有本地端口(lport)和远程端口(rport)之分,如果是做服务端,我们通过监听本地端口(lport)实现;如果是做客户端,则需要去连接远程端口(rport)。 6) 处理 uIP 事件 最后,uIP 通过 uip_polling 函数轮询处理 uIP事件。该函数必须插入到用户的主循环里 面(也就是必须每隔一定时间调用一次)。ENC28J60模拟时序接口:   
  • [技术干货] 基于STM32+华为云IOT设计的酒驾监控系统
    1. 前言随着人们生活水平的提高,为了减少和杜绝车的现象越来越多,所引发的交通事故也引起了人们的重视,为了减少和杜绝酒后驾车导致事故发生,当前设计了一种安装在驾驶室内,能根据具体功能进行鉴别酒精浓度的系统,当识别到酒驾之后会发出警报,并发送短信给指定紧急联系人,通知家人通知酒驾,一起监督行车安全。并将GPS数据上传到华为云物联网平台,调用地图模块显示具体位置,主控芯片采用STM32单片机,GSM模块采用IM800C。系统还带了一块LCD显示屏,可以实时显示检测的酒精含量,酒精超标后,本地蜂鸣器也会发出警报,提醒驾驶员。测量酒驾的标准是:(1)饮酒驾车是指车辆驾驶人员血液中的酒精含量大于或者等于20mg/100ml,小于80mg/100ml的驾驶行为。(2)醉酒驾车是指车辆驾驶人员血液中的酒精含量大于或者等于80mg/100ml的驾驶行为。需要使用的硬件如下:(1)STM32系统板(2)酒精检测传感器(3)LCD显示屏(4)供电模块(5)SIM800C模块(6)蜂鸣器模块(7)GPS模块--采用SIM800C内部的GPS功能获取GPS数据。为了测试,搞了几口二锅头。上位机采用QT开发,调用华为云的应用侧接口获取设备上传的数据。再调用百度接口,获取GPS定位的位置。2. 登录华为云创建云端设备2.1 创建产品华为运官网: https://www.huaweicloud.com/接下来继续创建产品,点击产品页面,点击右上角创建产品: 2.2 产品模型定义 2.3 生成MQTT登录密匙创建完产品、设备之后,接下来就需要知道如何通过MQTT协议登陆华为云服务器。官方的详细介绍在这里: https://support.huaweicloud.com/devg-iothub/iot_01_2127.html#ZH-CN_TOPIC_0240834853__zh-cn_topic_0251997880_li365284516112 MQTT设备登陆密匙生成地址: https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/ 2.4 使用MQTT客户端软件登录所有的参数已经得到,接下来采用MQTT客户端登录华为云进行测试。华为云物联网平台的域名是: a161a58a78.iot-mqtts.cn-north-4.myhuaweicloud.com华为云物联网平台的IP地址是: 121.36.42.100在软件里参数填充正确之后,就看到设备已经连接成功了。接下来打开设备页面,可以看到设备已经在线了。//订阅主题: 平台下发消息给设备 $oc/devices/625cd29cecf9c41c38215ba5_1126626497/sys/messages/down //设备上报数据 $oc/devices/625cd29cecf9c41c38215ba5_1126626497/sys/properties/report //上报的属性消息 (一次可以上报多个属性,在json里增加就行了) {"services": [{"service_id": "MQ3","properties":{"MQ3":50}}]}3. STM32代码实现酒精浓度传感器的预热时间要20秒~1分钟左右,这段时间之后采集的数据才会稳定。预热模块会发热,烫手,这个是正常现象。3.1 系统原理图3.2 代码#include "stm32f10x.h" #include "led.h" #include "delay.h" #include "key.h" #include "usart.h" #include <string.h> #include "timer.h" #include "oled.h" #include "adc.h" #include <string.h> #include <stdlib.h> #include "font.h" #include "sim800c.h" #include "mqtt.h" /* 硬件连接方式: 1. TFT 1.44 寸彩屏接线 GND 电源地 VCC 接5V或3.3v电源 SCL 接PC8(SCL) SDA 接PC9(SDA) RST 接PC10 DC 接PB7 CS 接PB8 BL 接PB11 2. 蜂鸣器 VCC--->3.3V DAT--->PA6 GND--->GND 3. 酒精检测传感器MQ3 VCC--->3.3V GND--->GND DAT--->PA1 4. 板载LED灯接线 LED1---PA8 LED2---PD2 5. 板载按键接线 K0---PA0 K1---PC5 K2---PA15 6. SIM800C--GSM模块 GND----GND VCC--->3.3V PA2----SIM800C_RXD PA3----SIM800C_TXD */ //华为物联网服务器的设备信息 #define MQTT_ClientID "62381267575fb713ee164ad2_xl_1_0_0_2022032106" #define MQTT_UserName "62381267575fb713ee164ad2_xl_1" #define MQTT_PassWord "124344feff3e3d96ff6af13cf36af36766619ff1eeee40e99cbae9b7b9739fe4" //订阅与发布的主题 #define SET_TOPIC "$oc/devices/62381267575fb713ee164ad2_xl_1/sys/messages/down" //订阅 #define POST_TOPIC "$oc/devices/62381267575fb713ee164ad2_xl_1/sys/properties/report" //发布 //设置连接的路由器信息 #define CONNECT_WIFI "Xiaomi_meizi6" //将要连接的路由器名称 --不要出现中文、空格等特殊字符 #define CONNECT_PASS "12170307yu" //将要连接的路由器密码 #define CONNECT_SERVER_IP "a161a58a78.iot-mqtts.cn-north-4.myhuaweicloud.com" //服务器IP地址 #define CONNECT_SERVER_PORT 1883 //服务器端口 //JTAG模式设置,用于设置JTAG的模式 //mode:jtag,swd模式设置;00,全使能;01,使能SWD;10,全关闭; #define JTAG_SWD_DISABLE 0X02 #define SWD_ENABLE 0X01 #define JTAG_SWD_ENABLE 0X00 void JTAG_Set(u8 mode) { u32 temp; temp=mode; temp<<=25; RCC->APB2ENR|=1<<0; //开启辅助时钟 AFIO->MAPR&=0XF8FFFFFF; //清除MAPR的[26:24] AFIO->MAPR|=temp; //设置jtag模式 } u16 MQ3_data=0; //酒精浓度 float MQ3_data_float=0; //mg/l char data_buff[100]; //酒精超标提示 u8 sim800c_buff[100]="Drunk driving reminder"; int main() { u8 state=0; u8 key=0; u32 time_cnt=0; JTAG_Set(JTAG_SWD_DISABLE); //释放PA15 LED_Init(); //LED灯初始化 BEEP_Init(); //蜂鸣器初始化 KEY_Init(); //按键初始化 USART1_Init(115200); //串口1初始化-打印调试信息 Lcd_Init(); //LCD初始化 Lcd_Clear(0); //清屏为黑色 LCD_LED_SET; //通过IO控制背光亮 AdcInit(); //ADC初始化--检测酒精传感器的值 TIMER2_Init(72,20000);//辅助串口2接收,超时时间为20ms USART2_Init(115200); //可能的波特率(测试): 57600 、9600、115200 USART3_Init(115200);//串口-WIFI TIMER3_Init(72,20000); //超时时间20ms //预热视频 LCD_ShowChineseFont(0,16*3,16,HZ_FONT_16[20],RED,0); LCD_ShowChineseFont(16,16*3,16,HZ_FONT_16[21],RED,0); LCD_ShowChineseFont(16*2,16*3,16,HZ_FONT_16[22],RED,0); LCD_ShowChineseFont(16*3,16*3,16,HZ_FONT_16[23],RED,0); LCD_ShowChineseFont(16*4,16*3,16,HZ_FONT_16[24],RED,0); LCD_ShowChineseFont(16*5,16*3,16,HZ_FONT_16[25],RED,0); LCD_ShowChineseFont(16*6,16*3,16,HZ_FONT_16[26],RED,0); LCD_ShowChineseFont(16*7,16*3,16,HZ_FONT_16[27],RED,0); //delay_ms(30000); //初始化时间30秒 delay_ms(5000); //初始化时间5秒 Lcd_Clear(0); //清屏为黑色 //采集数据的真实数据 LCD_ShowChineseFont(0,16*2,16,HZ_FONT_16[0],RED,0); LCD_ShowChineseFont(16,16*2,16,HZ_FONT_16[1],RED,0); LCD_ShowChineseFont(16*2,16*2,16,HZ_FONT_16[2],RED,0); LCD_ShowChineseFont(16*3,16*2,16,HZ_FONT_16[3],RED,0); //酒精含量计算结果 LCD_ShowChineseFont(0,16*3,16,HZ_FONT_16[4],RED,0); LCD_ShowChineseFont(16,16*3,16,HZ_FONT_16[5],RED,0); LCD_ShowChineseFont(16*2,16*3,16,HZ_FONT_16[6],RED,0); LCD_ShowChineseFont(16*3,16*3,16,HZ_FONT_16[7],RED,0); //酒精超标显示 Gui_DrawFont_GBK16(0,16*4,RED,0,(u8*)" "); LCD_ShowChineseFont(0,16*4,16,HZ_FONT_16[16],RED,0); LCD_ShowChineseFont(16,16*4,16,HZ_FONT_16[17],RED,0); LCD_ShowChineseFont(16*2,16*4,16,HZ_FONT_16[18],RED,0); LCD_ShowChineseFont(16*3,16*4,16,HZ_FONT_16[19],RED,0); //初始化SIM800C state=SIM800C_InitCheck(); printf("SIM800C初始化状态:%d\r\n",state); DelayMs(1000); //设置文本模式 state=SIM800C_SetNoteTextMode(); printf("设置文本模式状态:%d\r\n",state); DelayMs(1000); // printf("正在初始化WIFI请稍等.\r\n"); for(i=0;i<5;i++) { if(ESP8266_Init()==0) { esp8266_state=1; break; } else { esp8266_state=0; printf("ESP8266硬件检测错误.\n"); } } if(esp8266_state) { printf("准备连接服务器....\r\n"); //非加密端口 printf("WIFI:%d\n",ESP8266_STA_TCP_Client_Mode(CONNECT_WIFI,CONNECT_PASS,CONNECT_SERVER_IP,CONNECT_SERVER_PORT,1)); //2. MQTT协议初始化 MQTT_Init(); //3. 连接服务器 for(i=0;i<5;i++) { if(MQTT_Connect(MQTT_ClientID,MQTT_UserName,MQTT_PassWord)==0) { esp8266_state=1; break; } esp8266_state=0; printf("服务器连接失败,正在重试...\n"); delay_ms(500); } printf("服务器连接成功.\n"); //3. 订阅主题 if(MQTT_SubscribeTopic(SET_TOPIC,0,1)) { printf("主题订阅失败.\n"); } else { printf("主题订阅成功.\n"); } } while(1) { key=KEY_Scan(); if(key) { printf("key=%d\r\n",key); } //控制LED灯 if(key==2) { LED2=!LED2; } //手动控制蜂鸣器测试 //手动发送短信测试 if(key==1) { BEEP=1; delay_ms(100); BEEP=0; sprintf((char*)sim800c_buff,"Drunk driving reminder:%d",MQ3_data); //发送短信 if(SIM800C_SendNote((u8*)"13800138000",sim800c_buff,strlen((char*)sim800c_buff))==0) printf("短信发送成功\r\n"); else printf("短信发送失败\r\n"); } //轮询时间到达 if(time_cnt>300) { time_cnt=0; LED1=!LED1; //获取空气质量 MQ3_data=GetAdcCHxDATA(1); printf("采集MQ3数据:%d\r\n",MQ3_data); //上报数据 sprintf(data_buff,"{\"services\": [{\"service_id\":\"STM32\",\"properties\":{\"MQ3\":%d}]}", MQ3_data); MQTT_PublishData(POST_TOPIC,data_buff,0); //上报数据 sprintf(data_buff,"{\"services\": [{\"service_id\":\"STM32\",\"properties\":{\"MQ3\":%d}]}", MQ3_data); MQTT_PublishData(POST_TOPIC,data_buff,0); //LCD屏实时显示酒精浓度 //采集数据 sprintf(data_buff,"%d",MQ3_data); Gui_DrawFont_GBK16(72,16*2,RED,0,(u8*)" "); Gui_DrawFont_GBK16(72,16*2,RED,0,(u8*)data_buff); //酒精含量 //MQ3_data_float=((float)MQ3_data*(5.0/4096))*0.36-1.08; MQ3_data_float=((float)MQ3_data*(5.0/4096))*0.36; sprintf(data_buff,"%0.2f mg/L",MQ3_data_float); printf("计算酒精浓度:%s\r\n",data_buff); Gui_DrawFont_GBK16(72,16*3,RED,0,(u8*)" "); Gui_DrawFont_GBK16(72,16*3,RED,0,(u8*)data_buff); //判断酒精浓度是否超标,设置阀值 if(MQ3_data>2000) { sprintf((char*)sim800c_buff," :%d",MQ3_data); //发送短信 if(SIM800C_SendNote((u8*)"18171571217",sim800c_buff,strlen((char*)sim800c_buff))==0) printf("短信发送成功\r\n"); else printf("短信发送失败\r\n"); } //采集的值越小,表明酒精浓度越高。反之越大。 //根据阀值显示不同的文字提示 if(MQ3_data>2000) { //你已酒驾 Gui_DrawFont_GBK16(0,16*4,RED,0,(u8*)" "); LCD_ShowChineseFont(0,16*4,16,HZ_FONT_16[8],RED,0); LCD_ShowChineseFont(16,16*4,16,HZ_FONT_16[9],RED,0); LCD_ShowChineseFont(16*2,16*4,16,HZ_FONT_16[10],RED,0); LCD_ShowChineseFont(16*3,16*4,16,HZ_FONT_16[11],RED,0); } //正常稳定范围采集的值是500左右 else { //安全范围 Gui_DrawFont_GBK16(0,16*4,RED,0,(u8*)" "); LCD_ShowChineseFont(0,16*4,16,HZ_FONT_16[16],RED,0); LCD_ShowChineseFont(16,16*4,16,HZ_FONT_16[17],RED,0); LCD_ShowChineseFont(16*2,16*4,16,HZ_FONT_16[18],RED,0); LCD_ShowChineseFont(16*3,16*4,16,HZ_FONT_16[19],RED,0); } } DelayMs(10); time_cnt++; } }
  • [行业资讯] 单片机未来趋势瞻仰,出货量说明了一切
    单片机未来趋势瞻仰,出货量说明了一切从数据来看,出货量为什么会持续增长,主要是物联网和5G的技术在逐渐成熟,带动了很多传统产品和新型应用的转型,比如智慧停车场、门禁、共享充电宝、共享单车、充电桩、智能家居等等等等。现在其实还不是正在爆发的时候,因为物联网现在也还存在着许多技术瓶颈没有突破,还没有非常成熟,比如延迟的问题、安全性的问题、功耗的问题。等到这些都成熟的的时候,或许就是单片机的寒冬真正过去,春天即将到来,所以奠定了一定的基础,对未来的发展会有一定的帮助。对单片机感兴趣的朋友可以找我,我录制了一些关于单片机的入门教程,有需要的童鞋找我拿就像,免费的,私信我“林老师”就可以拿~点击打开我的头像就能领取下面分享一些关于红外遥控的应用的知识,以单片机为例来给大家介绍红外遥控的编码和解码,专用的红外遥控器的编码芯片就不再做介绍了,这里主要是用单片机用I/O口来仿真的办法来做红外遥控的发射,同时会给再讲解一下,怎么样用单片机来做红外接收的解码,先介绍用单片机来做红外遥控的发射,然后再给介绍红外遥控的解码。红外遥控的发射,可以选用专用的编码芯片,我这主要是用单片机的I/O口来模拟,大家学习的时候,对于理解红外遥控的原理,个人觉得用这种办法的话,会更容易理解一点,利用单片机来制作简单的红外遥控器,先给大家简单的介绍一下红外的概念,然后对这个硬件电路的设计做简单的介绍,然后再给大家简单的介绍一下我们经常使用的协议,也就是红外协议,有兴趣的可以找我聊天,教大家如何用单片机来编写代码,然后来实现简单的红外遥控,之后再给大家介绍如何来做红外的解码。首先来简单的看一下红外遥控的简介,什么是红外线?红外线实际上就是一种肉眼看不见的光,就是红外光,红外线是介于波长在760纳米到1毫米之间的,它是介于微波和可见光之间的一种电磁波,是非可见光,人肉眼也看不到,家里空调或者是电视机的遥控器,当按下按键的时候,你可以拿手机打开手机的摄像头,可以把遥控器对着手机的摄像头,那你可以在屏幕当中是可以看到这个红外光的。红外遥控它是一种无线的非接触的控制技术,具有抗感染能力比较强,传输比较可靠,然后功耗低成本比较容易实现,所以在家用电器当中,包括现在的一些物联网的设备当中,以及在现在的工业设备当中都得到了广泛的应用,所以我们有必要来了解一下红外的概念,以及如何来通过单片机来做简单的红外的发射和红外的接收。如何产生红外光?可以采用红外发射二极管,红外发射二极管它可以发出红外线的一种光,当然了红外发射二极管实际上和普通的发光二极管差不多,它的使用的办法也差不多,最常用的红外发射管是发出940纳米红外光的,还有一种是850纳米红外光的,大家可以去某宝上或者一些气电商城去搜这种红外线的管子,可以看到它们常见的这种管子,基本上都是940纳米的红外管,大家可以选用这种类型的管子。这种管子,它和我们普通的发光二极管差不多,但是它发射出来的是红外光,普通的发光二极管,发出来的可能是红光绿光,这个它发出的是红外光,红外光人也看不到,但是也可以做实验,刚才说了可以拿手机的摄像头对准它,在屏幕当中能够看到这个红外光,同样道理,发光二极管的话,它要通过一定的电流才能导通,它要有一定的导通压降,同时要有一定的电流它才能够发光。
  • [问题求助] 【温湿度远程监测】【温湿度上传到华为云】华为云平台下发命令esp8266接收不到
    【功能模块】华为云平台下发命令,stm32+ESP8266利用MQTT协议接收,然后做出对应的操作【操作步骤】1.用esp8266连接wifi,连接华为云平台服务器  【测试无误】2.订阅相关主题,准备向云平台索求命令mqtt的订阅主题Topic测试了共两个:①$oc/devices/624a6b8e2d0897328702a1f6_weason-agriculture/sys/commands/#  【核心问题】 参考依据是②$oc/devices/624a6b8e2d0897328702a1f6_weason-agriculture/user/dingyue 【核心问题】这个是自定义topic经测试,这两个Topic均无法下达命令 订阅主题函数为【此函数经测试可以订阅其他服务器的主题】【经测试,ESP8266_GetIPD函数没有get到任何值】华为云平台返回的数据的格式是那种【子问题1】3.向服务器上传温湿度数据(pub),经验证无误,可正常上传。【问题汇总】核心问题为:为什么收不到华为云平台下发的命令子问题为:①华为云平台返回的数据的格式是那种【子问题1】②我所订阅的主题是否正确③观察 消息跟踪 得到其Topic与我所订阅的一致,但确实未收到消息。综上问题,我将STM32的程序源码添加至附件了,Readme.txt中有硬件的连接。望诸位同志斧正。急急急
  • [行业资讯] 物联网专业真的很坑吗?浅谈物联网的未来发展趋势和方向
    人啊,最怕就是人云亦云,有时你看到的不一定是真的,其实最好的办法就是看一下这个行业的从业者,他们混得怎么样,多找几个综合对比,比你看任何文章都要来得靠谱!我仔细看了一些回答者以及一些评论者,基本可以判断,这些人要么是在校学生,要么就是外行人。真正这个行业的资深人士都在研究怎么做产品,怎么融资,怎么闷身发财。一、电子展让我学到很多在2014年的时候,当时在一家公司做物联网产品线的项目经理兼研发,经常会参加一些展会,以便自己熟悉行业的人都在做什么产品。大家现在知道那些发展得还不错的平台和公司,其实在那个时候还是个小公司。也正是抓住了物联网这波红利,一飞冲天,特别是现在的物联网云平台。他们快速起飞的秘诀呢,就是不断路演,然后融资。其实当时的所谓物联网产品真的很鸡肋。拿当时wifi插座来说,就开关这么简单的功能,我要先花几分钟配置wifi让插座连上家里的wifi,连上以后打开app,等app连上服务器以后才能控制。有时出问题搞个10几分钟都开不了一个插座,还不如自己走过去手动打开关闭来得快,碰到这种情况你可能会崩溃想把它砸了。我记得当时做wifi插座的时候,不管是wifi模块厂家,还是云平台,都是求着一些传统硬件厂家去使用他们模块和接入他们平台的。当时传统硬件接入平台也不用license费用的,不像现在每接入一个硬件设备到他们平台都要3-10块钱不等。当时大家都在做实验,即便只是一个wifi插座,远程控制开关这么简单的功能,很多都做不稳定。我当时做得也非常痛苦,对这种产品又爱又恨,我们硬件端基本已经很成熟了,因为产品都是经过大批量验证,只不过在原来的基础上加上wifi模组给产品赋能实现远程控制而已。但是中间涉及的环节太多,坑也多,有碰到因为协议不成熟导致不稳定的,也有因为app有问题导致不稳定的。所以做物联网产品是整个体系,其中一个环节不给力,你的努力可能都会白费,这也是为什么很多所谓的远程控制产品都是鸡肋的原因。经历了这么多年的研发和产品验证,我基本上也熟悉了硬件端实现物联网整个通讯流程和协议,后面也和一家企业联合从零搭建了物联网云平台。随着后面对接的平台越来越多,发现各大平台的通讯协议,其实都是相互模仿改进的。二、为什么很多人觉得物联网专业很坑?说了这么多,如果你是物联网工程专业,你可能会觉得很懵,我说的这些怎么我都没接触过?难道我是读的假专业?你有这种问题实在太正常了,不是你学的假专业,而是学校学得太杂了。比如说C/C++、java、C#、单片机原理与应用、传感器原理与应用、ZigBee无线网络技术等等。所学这些很多完全是不同产业链,不同行业方向的知识,这样学,你根本不知道物联网专业出来是干嘛的,跟四不像似的。很多人学完出来甚至连自己找什么职位都不知道,找不到工作也是正常的,所以觉得这个专业坑。如果你单靠学校学出来找工作,未免太牵强了,物联网涉及的领域和行业太多了,要先定位好方向,然后针对性深造。想从事物联网行业,我认为嵌入式是一个不错的切入点。记得在2014年,也就是物联网开始火爆的时候,大量的人都涌去学java做app,当时需求量也确实很大。当时做产品经常要和app工程师联调,有时候一个功能怎么做都做不稳定,我都觉得他们技术很水,但是工资都比我们做单片机开发的要高几千块。那段时间让我非常郁闷,我和很多从业人员一样,一遍靠着这个行业吃饭,一遍骂着这个行业辣鸡,软硬件都要懂,工资还比做app的低这么多。不过最近两年我和一个做app的朋友对比,发现做app也没以前这么香了,反而是做嵌入式的一直处于上涨趋势,很多已经超过app。嵌入式也分很多方向的,主流的是就是单片机开发或者嵌入式linux开发。所以,如果你想从事物联网这个行业,一定把方向定得越细越好,比如说我只做单片机开发。然后用单片机技术去从事物联网的产品开发,这样你才能真正切入物联网这个行业。比如说无线通讯技术(315/433/868M、Lora、Zigbee、蓝牙、nb-iot、wifi等等).这些无线通讯技术怎么应用到我们产品上呢?比如说我们无际单片机编程带领学员做的智能防盗报警系统。主机再通过wifi模块与云平台连接,实现远程监控,这就是一套典型的物联网产品。通过这种项目切入物联网是最接地气的,只有当你做过这样的产品,你才会深刻理解自己学的东西到底有啥用。三、物联网未来发展趋势和方向从目前看的话,虽然这个行业已经发展了很多年,但是我认为依然算得上是蓝海行业。因为目前技术还没有完全成熟,很多东西,没有基础铺垫,即便你知道未来一定会这样去发展,你当下也实现不了。我举个例子,比如说美团、滴滴,包括很多线上的生意,如果线上支付没出来,这种些肯定也很难做起来,因为没有支付工具的基础铺垫。我经常说,我们未来的生活场景就是物联网技术的体现。很多人可能还不知道,物联网到底有啥用,能给我们带来什么便利。下面我还是通过大家肯定接触过的一个例子来讲解:共享单车。共享单车就是一个很典型的物联网应用,并且也确实解决了我们短途出行的效率,这个不可否认。大家想象一下,如果每个个体的出行效率提高了,那必定会提高整体的效率,其中能产生的潜在价值细思极恐。还有其他的智慧停车场之类的物联网应用就不说了,相信经常开车出行的朋友都试过自主扫码付费出停车场,真的是太方便了。所以,物联网目前的应用最大的价值在于提高效率的同时降低成本,只要能让人类更进步的技术我都认为是趋势。而现在物联网仍然处于蓝海阶段,还有很多技术瓶颈尚未解决,lora、nb-iot之类技术的出现都是为了解决物联网某个环节的痛点。等真正实现万物互联那一天,估计就没我们啥事了。当然,如果你能在红利前期布局好,不管是技术还是产品,沉淀几年,别人想超越你也是没那么容易的,这才是核心竞争力。
  • [技术干货] eMTC,你所不了解的基础
    eMTC,全称是 LTE enhanced MTO,是基于LTE演进的物联网技术。为了更加适合物与物之间的通信,也为了更低的成本,对LTE协议进行了裁剪和优化。eMTC基于蜂窝网络进行部署,其用户设备通过支持1.4MHz的射频和基带带宽,可以直接接入现有的LTE网络。eMTC支持上下行最大1Mbps的峰值速率,可以支持丰富、创新的物联应用。eMTC的基本特性窄带LTE其中最主要的几个特性。第一,系统复杂性地大幅度降低,复杂程度及成本得到了极大的优化。第二,功耗极度降低,电池续航时间大幅度增强。第三,网络的覆盖能力大大加强。第四,网络覆盖的密度增强。eMTC具备LPWA基本的四大能力:一是广覆盖,在同样的频段下,eMTC比现有的网络增益15dB,极大地提升了LTE网络的深度覆盖能力;二是具备支撑海量连接的能力,eMTC一个扇区能够支持近10万个连接;三是更低功耗,eMTC终端模块的待机时间可长达10年;四是更低的模块成本,大规模的连接将会带来模组芯片成本的快速下降,eMTC芯片目标成本在1~2美金左右 。除此之外,eMTC还具有四大优势:一是速率高,eMTC支持上下行最大1Mbps的峰值速率,远远超过GPRS、 ZigBee等物联技术的速率,eMTC更高的速率可以支撑更丰富的物联应用,如低速视频、语音等;二是移动性,eMTC支持连接态的移动性,物联网用户可以无缝切换保障用户体验;三是可定位,基于TDD的eMTC可以利用基站侧的PRS测量,在无须新增GPS芯片的情况下就可进行位置定位,低成本的定位技术更有利于eMTC在物流跟踪、货物跟踪等场景的普及:四是支持语音,eMTC从LTE协议演进而来,可以支持 VOLTE语音,未来可被广泛应用到可穿戴设备中。原理与关键技术1、物理层资源结构eMTC作为LTE一个特性, 基本沿用LTE设计,占原有LTE系统的6个PRB,其中一个RB占12个子载波 (子载波带宽15kHz, 间隔为15kHz) 。时域结构上eMTC帧结构与LTE一致;频域结构上,3GPP将系统带宽划分成若干NB (不重叠的6个PRB) ,eMTC UE的调度受NB限制,不能跨NB调度,不同eMTC UE可以共享一个NB的资源。2、信号与信道eMTC不重用LTE的PDCCH、PCFICH和PHICH下行信道, 新增MPDCCH信道, 用于发送eMTC UE的PDSCH和PUSCH信道的调度指示以及公共消息的指示, 比如寻呼、RAR响应、上行ACK反馈。eMTC重用LTE的下行数据信道PDSCH, 支持传输模式为TM1/2/6/9;eMTC重用LTE的下行导频信号RS;重用LTE的物理同步信号PSS/SSS, 其中PSS映射到时隙0和时隙10的最后一个OFDM符号, SSS映射到时隙0和时隙10的倒数第二个OFDM符号,均以5ms为周期重复发送;eMTC重用LTE的物理广播信道PBCH, 新增一套SIB消息, 包括SIB1-BR、SIB2、SIB3、SIB4、SIB5和SIB14共6条,MIB消息新增一个IE用于携带SIB1-BR的调度信息, 在每个系统帧的0#子帧和9#子帧发送, 周期为40ms。eMTC的PRACH和LTE的PRACH分开 (使用相同频率, 时域上区分) , 可以采用时分, 频分, 码分方式;eMTC的PUCCH和LTE的PUCCH分开, eMTC的PUCCH支持跨子帧跳频, 不支持子帧内跳频;eMTC使用LTE传统的PUSCH信道上传数据资源, 其PUSCH资源受NB限制。3、资源共享与调度eMTC作为小区特性, 与LTE共小区部署, 不占用独立小区, 但是需要占用空口的RB资源和基带的处理资源, 为保证MBB业务优先, 系统会预留一定的资源给LTE, 即使LTE没有任何业务, eMTC也不能使用预留。通过配置参数EmtcDlRbTargetRatio和EmtcUlR bTargetRatio, 可以控制LTE和e MTC资源占用比例, 在LTE和eMTC负载均很高时, 依据两者目标利用率, 动态共享LTE的PRB资源, 如图3-case1;当eMTC负载较高, 而LTE有空闲RB资源时, 这些空闲RB资源可以给eMTC使用, 如图3-case2;因e MTC采用跨子帧调度和重复技术, 会长期占用RB资源, 为了避免LTE控制消息和VoIP等高优先级业务被长期阻塞, 通过DlLteRvsNbNum和UlLteRvsNbNum参数给LTE预留RB资源, 保证LTE业务的需求, 即LTE负载较而eMTC负载较低时, LTE可以占用全部带宽。4、峰值速率与LTE下行异步HARQ, 上行同步HARQ不同, eMTC上行下行都是异步HARQ。下行调度, 设MPDCCH重复的最后一个子帧编号n, 则MPDCCH调度的PDSCH起始子帧编号为n+2;设PDSCH重复的最后子帧编号为n, 则PUCCH 1 Ack/Nack子帧编号为n+4。上行调度, 设MPDCCH重复的最后一个子帧编号n, 则MPDCCH调度的PUSCH起始子帧编号为n+4;设PUSCH重复的最后子帧编号为n, 则MPDCCH Ack/Nack子帧编号为n+4。在无重复及重传的情况下,以ModeA对eMTC速率进行估算:下行调度周期为10ms, 全双工时可以下行连续MPDCCH和PDSCH调度, 10ms周期内能发送8个下行TB (传输块) , 每个TB最大1000bits,因此下行峰值速率为8*1000* (1000/10) =800kbps;同理下行半双工峰值速率为300kbps;上行行调度周期为8ms, 同理推算出全双工上行峰值速率1000kbps, 半双工上行峰值速率375kbps。5、功耗eMTC采用PSM和e DRX技术以节约功耗。PSM是一种新增的比Idle态更省电的省电模式, 由MME通过NAS配置给UE, UE发送完数据后在Idle态停留一段时间后进入深度睡眠态, 不监听任何空口消息, 只在主动发送数据和周期TAU时才退出PSM模式, 如图5 (a) ;eDRX通过延长Idle态或连接态的DRX周期, 减少UE侦听网络的信令处理, UE只在每个e DRX周期只在寻呼窗口内监听PDCCH, 其它时间处于睡眠状态, 从而达到UE节电的目的。应用场景eMTC是爱立信提出的无线物联网解决方案。eMTC基于LTE接入技术设计了无线物联网络的软特性,主要面向中低速率、低功耗、大连接、移动性强、具有定位需求的物联网应用场景。eMTC无线物联网技术可支持语音、移动、定位业务,适合进行速率为100kbit/s~1Mbit/s范围内的中速小包数据或语音业务,模组市场价约10美元每块,典型应用为智能电梯、行车、物流跟踪、穿戴设备等。
  • [交流分享] 聊聊万物互联-Wi-Fi6
    WiFi的历史从802.11的FHFS,DSSS到802.11b的DSSS,到802.11a的OFDM,802.11g的ERP(将OFDM从5G迁移到了2.4G),到802.11n的更宽频带(40MHz)的OFDM技术,到802.11ac的进一步拓宽(80, 80+80,160MHz)的OFDM技术,到802.11ax的更窄的子载波(78.125kHz)的OFDM技术,到讨论中的802.11be的320MHz的OFDM技术。OFDM技术从802.11a,大概2001年的时候,到现在,已经持续演进了20年了,它的抗多径效应,适合无线空间的复杂环境。因为802.11ax(Wi-FI 6)希望覆盖更广的空间,所以把载波宽度进一步变小了。每一代的发展,频带基本是越来越宽,似乎是可以无限的把频宽扩展下去。但是这应该是存在问题的,多样化的需求下,大频宽是可能浪费频段的。毕竟有些地方只需要小的频宽就好了。802.11ax还定义了一个仅支持20MHz的模式,也是瞄准了万物互联的趋势下,小数据,低能耗的搭配。但是802.11be(TB Wi-Fi 7)又把带宽变得更大了,每一代总是希望能更快的。但是单纯更快有什么意思呢。OFDM不止可以分频带,因为网络的特性,还可以分用户,这是Wi-Fi现在越来越看重的,希望越来越多的用户都是使用这个网络,而且还要能用。从802.11ac(2013)开始的Wi-Fi5已经引入了多用户的观念,对应的技术是MU-MIMO。它是这么样的,不同的用户用的是不同的天线。到了802.1ax(Wi-Fi6),支持上行的MU-MIMO,而且还把一个频段同时分配给了多个用户(OFDMA),每个用户最少可以只用2MHz的带宽。一下还把上行和下行都给加进去了。不止如此,还加入了BSS Coloring技术,弱化Exposed Node的问题,提升密集网络覆盖下的并行性。Wi-Fi6有这么多优势,其实它的主要着笔点在于密集用户,密集网络。想想现在Wi-Fi6的设备已经都出来1两年了,但是也并没有多改变生态。其实现在大家也觉得够用了,就像我自己,这种Wi-Fi6的理念对人这个用户来讲真的是有点超前了,哪里去找如此密集的人流和网络?办公场所,体育场?这些场合毕竟是少数。自动化生产车间也许是比较合适的,不过不是人流,而是物流。Wi-Fi7似乎要在这个道路上越走越远,越来越远…
  • [技术干货] 单片机里常见的通信协议详解
    单片机各种通信协议详解一、IIC通信协议(1)概述I2C(Inter-Integrated Circuit BUS) 集成电路总线,该总线由NXP(原PHILIPS)公司设计,多用于主控制器和从器件间的主从通信,在小数据量场合使用,传输距离短,任意时刻只能有一个主机等特性。经常IIC和SPI接口被认为指定是一种硬件设备,但其实这样的说法是不尽准确的,严格的说他们都是人们所定义的软硬结合体,分为物理层(四线结构)和协议层(主机,从机,时钟极性,时钟相位)。IIC,SPI的区别不仅在与物理层,IIC比SPI有着一套更为复杂的协议层定义。下面来分别说明一下IIC的物理层和协议层。(2)  IIC的物理层A 只要求两条总线线路,一条是串行数据线SDA,一条是串行时钟线SCL。(IIC是半双工,而不是全双工)。每个连接到总线的器件都可以通过唯一的地址和其它器件通信,主机/从机角色和地址可配置,主机可以作为主机发送器和主机接收器。IIC是真正的多主机总线,(而这个SPI在每次通信前都需要把主机定死,而IIC可以在通讯过程中,改变主机),如果两个或更多的主机同时请求总线,可以通过冲突检测和仲裁防止总线数据被破坏。传输速率在标准模式下可以达到100kb/s,快速模式下可以达到400kb/s。连接到总线的IC数量只是受到总线的最大负载电容400pf限制。二、SPI 协议通常SPI通信要求4根线,分别是MOSI(mast output salve input), MISO, CLK, CS。当发送和接受数据的工作都准备好了,只要有时钟CLK,就发送数据,没有时钟就不发送,而且一个时钟周期发送一位(bit)数据,所以发送数据的快慢由时钟频率来控制。3.至于时钟和数据的相位没有特别严格的要求(而IIC中,数据的变化只能在SCL是低电平的时候发生),SPI数据的变化是一个时钟周期一次,这样的方法来传输数据就简单多了。我们可以根据需求对时钟的极性和相位做调整,看看是在时钟上升沿还是下降沿来发送数据,还有停止发送时时钟的极性,是保持高电平还是低电平。4.另外在多机通信时,SPI只是简单的通过一个片选信号来选择哪个设备占用总线,但是IIC是通过发送从设备地址来自动选择的。三、什么是TTL电平、CMOS电平?TL电平信号被利用的最多是因为通常数据表示采用二进制规定,+5V等价于逻辑"1",0V等价于逻辑"0",这被称做TTL(晶体管-晶体管逻辑电平)信号系统,这是计算机处理器控制的设备内部各部分之间通信的标准技术。CMOS电平和TTL电平:CMOS电平电压范围在3~15V,比如:当5V供电时,输出在4.6以上为高电平,输出在0.05V以下为低电平。输入在3.5V以上为高电平,输入在1.5V以下为低电平。而对于TTL芯片,供电范围在0~5V,常见都是5V,如74系列5V供电,输出在2.7V以上为高电平,输出在0.5V以下为低电平,输入在2V以上为高电平,在0.8V以下为低电平。因此,CMOS电路与TTL电路就有一个电平转换的问题,使两者电平域值能匹配TTL高电平3.6~5V,低电平0V~2.4VCMOS电平Vcc可达到12V四、RS-232协议RS232(异步传输标准接口),是个人计算机上的通讯接口之一,也称串口或串行通讯接口。由电子工业协会(Electronic Industries Association,EIA) 所制定的异步传输标准接口。通常 RS-232 接口以9个引脚 (DB-9) 或是25个引脚 (DB-25) 的型态出现,一般个人计算机上会有两组 RS-232 接口,分别称为 COM1 和 COM2。是目前最常用的一种串行通讯接口。标准RS232接口: 常用串口只需要TX与RX即可。USB转RS232串口线五、CAN总线CAN是控制器局域网络(Controller Area Network, CAN)的简称,是由以研发和生产汽车电子产品著称的德国BOSCH公司开发的,并最终成为国际标准(ISO 11898),是国际上应用最广泛的现场总线之一。 在北美和西欧,CAN总线协议已经成为汽车计算机控制系统和嵌入式工业控制局域网的标准总线,并且拥有以CAN为底层协议专为大型货车和重工机械车辆设计的J1939协议。CAN 是Controller Area Network 的缩写(以下称为CAN),是ISO国际标准化的串行通信协议。在汽车产业中,出于对安全性、舒适性、方便性、低公害、低成本的要求,各种各样的电子控制系统被开发了出来。由于这些系统之间通信所用的数据类型及对可靠性的要求不尽相同,由多条总线构成的情况很多,线束的数量也随之增加。为适应“减少线束的数量”、“通过多个LAN,进行大量数据的高速通信”的需要,1986 年德国电气商博世公司开发出面向汽车的CAN 通信协议。此后,CAN 通过ISO11898 及ISO11519 进行了标准化,在欧洲已是汽车网络的标准协议。CAN 的高性能和可靠性已被认同,并被广泛地应用于工业自动化、船舶、医疗设备、工业设备等方面。现场总线是当今自动化领域技术发展的热点之一,被誉为自动化领域的计算机局域网。它的出现为分布式控制系统实现各节点之间实时、可靠的数据通信提供了强有力的技术支持。CAN的报文格式在总线中传送的报文,每帧由7部分组成。CAN协议支持两种报文格式,其唯一的不同是标识符(ID)长度不同,标准格式为11位,扩展格式为29位。在标准格式中,报文的起始位称为帧起始(SOF),然后是由11位标识符和远程发送请求位 (RTR)组成的仲裁场。RTR位标明是数据帧还是请求帧,在请求帧中没有数据字节。控制场包括标识符扩展位(IDE),指出是标准格式还是扩展格式。它还包括一个保留位 (ro),为将来扩展使用。它的最后四个位用来指明数据场中数据的长度(DLC)。数据场范围为0~8个字节,其后有一个检测数据错误的循环冗余检查(CRC)。应答场(ACK)包括应答位和应答分隔符。发送站发送的这两位均为隐性电平(逻辑1),这时正确接收报文的接收站发送主控电平(逻辑0)覆盖它。用这种方法,发送站可以保证网络中至少有一个站能正确接收到报文。报文的尾部由帧结束标出。在相邻的两条报文间有一很短的间隔位,如果这时没有站进行总线存取,总线将处于空闲状态六、485总线在要求通信距 离为几十米到上千米时,广泛采用RS-485串行总线标准。RS-485采用平衡发送和差分接收,因此具有抑制共模干扰的能力。RS232串口可以与485之间互转,在单片机上使用485总线与使用RS232串口一样,需要使用芯片转换电平即可!七、Modbus通讯协议Modbus协议是应用于电子控制器上的一种通用语言。通过此协议,控制器相互之间、控制器经由网络(例如以太网)和其它设备之间可以通信。它已经成为一种通用工业标准。有了它,不同厂商生产的控制设备可以连成工业网络,进行集中监控。此协议定义了一个控制器能认识使用的消息结构,而不管它们是经过何种网络进行通信的。它描述了一个控制器请求访问其它设备的过程,如何回应来自其它设备的请求,以及怎样侦测错误并记录。它制定了消息域格局和内容的公共格式。当在同一Modbus网络上通信时,此协议决定了每个控制器需要知道它们的设备地址,识别按地址发来的消息,决定要产生何种行动。如果需要回应,控制器将生成反馈信息并用Modbus协议发出。在其它网络上,包含了Modbus协议的消息转换为在此网络上使用的帧或包结构。这种转换也扩展了根据具体的网络解决节地址、路由路径及错误检测的方法。此协议支持传统的RS-232、RS-422、RS-485和以太网设备。许多工业设备,包括PLC,DCS,智能仪表等都在使用Modbus协议作为他们之间的通信标准。Modbus具有以下几个特点:(1)标准、开放,用户可以免费、放心地使用Modbus协议,不需要交纳许可证费,也不会侵犯知识产权。目前,支持Modbus的厂家超过400家,支持Modbus的产品超过600种。(2)Modbus可以支持多种电气接口,如RS-232、RS-485等,还可以在各种介质上传送,如双绞线、光纤、无线等。(3)Modbus的帧格式简单、紧凑,通俗易懂。用户使用容易,厂商开发简单。
  • [行业资讯] 一文了解IOT超低功耗设计应用
    IOT低功耗设备设计大致为3个方面的设计:器件选型、电路设计、软件设计、续航寿命估算——器件选型典型的器件包括:单片机MCU、电源芯片、通讯模组等。单片机——1.选择具备多种低功耗工作模式的MCU,如国民技术N32G4FR系列MCU支持5种低功耗模式(Sleep,Stop0,Stop2,Standby,VBat),开启带有RTC唤醒的Stop模式可让功耗尽可能低;2.支持宽范围供电,如1.8-3.3V,在不需要大电流供电的模式下,使用1.8V供电可以让MCU处于更低功耗的状态;3.不使用的IO配置为模拟输入,模拟输入模式下漏电流最低;关于MCU的超低功耗设计,参考该篇文字《STM32芯片超低功耗设计思路》电源芯片——1.选择更高效率的电源IC,开关电源DC-DC的效率高于LDO,特别在高压差、大电流的情况下,DC-DC具备更高的能效优势,对于常供电的IC,关注静态电流值,对于带EN管脚的IC,关注Shutdown电流值;2.LDO的成本比DC-DC低,且在低压差、低电流的情况下,具备低功耗特性的LDO也可做考虑,如圣邦微的SGM2034,静态漏电流为1uA;关于LDO与DC-DC的效能优势对比,参考该篇文字《LDO与DC-DC 的入门理解》通讯模组——1.通讯模组中的MCU部分可参考单片机的的低功耗设计,本质上具备一致性;2.2.4G的通讯模组,ZigBee低功耗具备更大优势,BLE蓝牙Mesh这两年间也开始逼近ZigBee,WiFi则比较大,同等条件下,ZigBee的发射电流可以做到50mA以内,而WiFi的发射电流一般要大于300mA,加上心跳包对接时间的差异,具备快联特性的WiFi可能需要10ms,而ZigBee可能只需要3-5ms。3.通讯模组OTA的功耗 > 搜网功耗 > 静态功耗。另外,网关信号正常与异常,也会导致通讯模组在搜网时的功耗有所不同。电路设计1. 对于耗电比较大的器件,使用独立IC供电,并尽可能做到可独立关断供电回路,在非常供电的状态下切断供电回路;2. 对于上下拉电阻,在确保信号抗干扰度良好的前提下,尽可能使用高阻值;如对于1K的上拉电阻,当电流回路对地时,产生3300uA的电流,而对于100K的上拉电阻,则为33uA。当然,对于外界的工频干扰等,同样的条件下,10K的上拉电阻具备更高的抗干扰度;3.电池电量检测采用分压电阻时可使用1M左右的阻值,由于涉及单片机ADC阻抗匹配的不同(关于ADC阻抗匹配,可参考《单片机读取外部电压ADC阻抗匹配问题》),建议在信号的采集中间加上一级电压跟随器,该跟随器需要低功耗或者需要单独供电,避免无谓的电量损耗;4.对于有光显示的场景,如LED指示等,尽可能降低LED亮度。软件设计软件设计更多地体现在如何驱动硬件进入低功耗模式,如:开启单片机RTC唤醒的Stop模式;控制电源的EN管脚进入非常供电模式;GPIO的模拟输入模式;通讯模组在发送完成数据之后,立即关闭UDP连接,尽可能降低大电流模式持续时间续航寿命估算1.对于静态电流,可使用万用表进行测量(如Fluke的17B+),由于万用表的采样率较低,且所呈现的数值为测量有效值,因此对于动态电流,如设备的间隔性心跳包电流,则需要使用采样率更高的仪器进行测量,如Keysight的N6705C;(关于低功耗测量仪器,可参考《浅谈4款低功耗电流测试“神器”》)2.严谨的功耗计算中,需考虑电池的自放电率,即电池即使在不使用的条件下,自身的电化学物质也会产生一定的反应自我消耗,特别是可充电的镍镉电池;3.简单举一个低功耗设备续航时间计算的例子:假设电池容量250mAh,10分钟发送一次心跳包对接网络,每次5秒30mA瞬时电流,待机20uA电流,可做如下推算:单次对接网络耗电:30mA x 5s = 150mAs = 41.66uAh;一天对接网络次数:(24h x 60)÷10 = 144次;一天对接网络总时间:5s x 144 = 720s;一天待机总时间:(24h x 3600)s - 720s = 85680s = 23.8h;一天总功耗:(23.8h x 20uA) + (144 x 41.66uAh) = 6475.04uAh = 6.48mAh;可使用天数:250mAh ÷ 6.48mAh ≈ 39天原创不易,如果我的文字对你有所启发或帮助,还请“点赞、收藏、转发”三连!
  • [数据库] Google test 介绍
    Googletest Primer1. Googletest简介1.1 特点目标语言:C++平台:Linux/Windows/Mac2.2 优势1. 测试独立可重复。允许单独跑某一个测试用例,方便debug。2. 组织清晰。相关联的测试用例可以组织在一起,共享数据。3. 平台中立。可用于不同的OS、编译器。4. 提供错误的更多信息。在不出现“致命”错误的情况下继续跑下一个测试用例。2. Googletest命名法    Test/Test Case/Test Suite,测试/测试用例/测试套,傻傻分不清楚。    常规理解的测试用例(Test Case)在googletest中叫测试(Test),测试套(Test Suite)叫测试用例(Test Case)。3. Assertion断言3.1 基本概念断言用来检查某个条件是否成立,是googletest的基础。一个断言的结果有3种状态:success,nonfatal failure和fatal failure。断言结果为fatal failure时,程序立即终止;其他情况程序继续运行。一个Test通过断言来验证代码的行为是否如预期。如果一个Test崩溃或者有失败的断言,则此Test失败。一个Test Case包含一个或多个Test。Test应该被组织成多个Test Case以反映被测代码的结构。当一个Test Case中的多个Test需要共享数据或者例行程序时,可以把他们组织成一个test fixture class。一个测试程序可以包含多个Test Case。3.2    Assertion详解Googletest中的assertion实际上就是宏。当一个断言失败时,googletest除了会打印出失败信息外,还会打印出失败断言的源文件以及代码行数定位。此外,用户还可以定制失败信息,定制的失败信息将追加在googletest自带的失败信息后。Googletest中有两类断言:ASSERT_*和EXPECT_*。ASSERT_*将在失败时生成fatal failure,并立即终止测试程序;而EXPECT_*生成nonfatal failure,当前的测试程序并不会终止。推荐使用EXPECT_*,因为它允许在一次测试中报告多个失败断言;但是当一个断言失败,其后续的断言的运行无意义时,则应该使用ASSERT_*。由于ASSERT_*会立即终止测试程序,后续的清理程序可能会被跳过,可能会造成内存泄漏。所以当你除了断言错误还得到了heap check error时,记得有内存泄漏的风险并自己评估是否有修复的必要。可以在断言后使用一个或多个 << 操作符打印定制错误信息,示例如下:3.1.%3 基础断言以下断言做最基本的true/false条件判断。Fatal assertionNonfatal assertionVerifiesASSERT_TRUE(condition);EXPECT_TRUE(condition);condition is trueASSERT_FALSE(condition);EXPECT_FALSE(condition);condition is false3.2.%3 对比断言这类断言比较两个值之间的大小Fatal assertionNonfatal assertionVerifiesASSERT_EQ(val1, val2);EXPECT_EQ(val1, val2);val1 == val2ASSERT_NE(val1, val2);EXPECT_NE(val1, val2);val1 != val2ASSERT_LT(val1, val2);EXPECT_LT(val1, val2);val1 < val2ASSERT_LE(val1, val2);EXPECT_LE(val1, val2);val1 <= val2ASSERT_GT(val1, val2);EXPECT_GT(val1, val2);val1 > val2ASSERT_GE(val1, val2);EXPECT_GE(val1, val2);val1 >= val2    对比断言中的值必须是可比较的,否则会得到编译错误。    如果用户自定义类型定义了比较操作符的含义,对比断言也适用。由于Google C++ Style Guide不鼓励这种做法,可以使用ASSERT_TRUE()或者EXPECT_TRUE()来判断两个用户自定义类型的值大小。    使用时,ASSERT_EQ(actual, expected)的优先级高于ASSERT_TRUE(actual == expected),因为对比断言失败时会打印出实际值和期望值。    ASSERT_EQ()也可以用于比较两个指针。如果是比较两个字符串,ASSERT_EQ()比较二者的内存地址是否相等。如果想比较两个字符串的内容是否相等,不要使用ASSERT_EQ(),而可以用ASSERT_STREQ()。如果想判断一个字符串是否为空,使用ASSERT_STREQ(c_string, NULL)。如果支持c++11标准,还可以使用ASSERT_EQ(c_string, nullptr)。用 *_EQ(ptr, nullptr) 或 *_NE(ptr, nullptr) 进行指针比较,不要用 *_EQ(ptr, NULL) 和*_NE(ptr, NULL)。3.3.%3 字符串断言这类断言比较两个字符串的内容。Fatal assertionNonfatal assertionVerifiesASSERT_STREQ(str1, str2);EXPECT_STREQ(str1, str2);两个字符串内容相同ASSERT_STRNE(str1, str2);EXPECT_STRNE(str1, str2);两个字符串内容不同ASSERT_STRCASEEQ(str1, str2);EXPECT_STRCASEEQ(str1, str2);不考虑大小写,两个字符串内容相同ASSERT_STRCASENE(str1, str2);EXPECT_STRCASENE(str1, str2);不考虑大小写,两个字符串内容不同    在这类断言中的”CASE”是忽略大小写的含义。一个空指针和一个内容为空的字符串含义不同。4. 写一个简单的测试创建一个测试的步骤为:1. 用TEST()宏来定义和命名一个测试程序,定义的测试程序就是普通的无返回值C++程序。2. 使用googletest assertion来检查值。3. 测试的结果取决于断言。如果有断言失败(fatally或non-fatally),或者程序崩溃,测试失败。所有断言成功通过,则测试成功。    TEST()的参数由宽泛到具体。第一个参数是Test Case的名字,第二个参数是此Test Case内的Test名称。所有的名称必须是有效的C++标识符,并且不能包含下划线。一个Test的完整的名称由它所属的Test Case和它自己的名称共同决定。不同Test Case内的Test可以有相同的名称。    以一个简单函数举例如何写TEST。    函数:    相应的测试用例:    Googletest根据测试用例来组织测试结果,因此逻辑相关的测试应该在同一个测试用例中。测试和测试用例的命名规则应该遵守Google C++编码风格。5. Test Fixtures    Test fixture可以使多个测试共享数据和配置。    创建fixture的步骤:1. 创建一个类,继承自::testing::Test,并用protected关键字修饰类体。2. 在类中声明后续测试将共享的数据对象。3. 必要时,写一个默认的构造器或者SetUp()函数来准备每个测试需要的对象。4. 必要时,写一个析构器或者TearDown()函数来释放在SetUp()函数中分配的资源。5. 必要时,定义测试共享的subroutine。当使用fixture时,测试应写成TEST_F()而不是TEST()。TEST_F()才能获取到test fixture中的共享数据和subroutine。    TEST()中第一个参数是所属测试用例名,而TEST_F()中第一个参数时test fixture class的名称。所以在TEST_F()之前就要定义好要使用的test fixture class。    对每一个以TEST_F()方式定义的测试,googletest都会在运行时生成一个新的test fixture,并通过SetUp()函数来初始化、TearDown()函数来执行清理工作。即使是在同一个测试用例中的测试,它们在运行时都会有新的不同的test fixture对象,因此一个测试对fixture所作的修改不会影响到其他测试。    举例: FIFO队列类首先,定义一个fixture class。Fixture class的命名通常为FooTest,Foo为要进行测试的类。    编写的测试如下:在此例中,既使用了ASSERT_*,也使用了EXPECT_*。如果你想要测试程序继续下去以提供关于代码的更多测试信息,就使用EXPECT_*,但是当测试程序继续运行已经没有意义时,还是要使用ASSERT_*。当这些测试运行时,会发生以下事件:1. Gooletest创建一个QueueTest对象(这里简称为t1);2. t1.SetUp()初始化t1;3. 第一个测试(IsEmptyInitially)运行在t1上;4. 测试结束时t1.TearDown()进行清理工作;5. t1被销毁;6. 又一个t1被创建,第二个测试(DequeueWorks)运行于其上,上述步骤再重复一遍。6. 运行测试TEST()和TEST_F()隐含了向googletest注册的步骤。定义好测试后,可以通过RUN_ALL_TESTS() 运行。如果所有的测试均通过,RUN_ALL_TESTS() 返回0;反之,返回1。RUN_ALL_TESTS() 会运行你所连接的所有测试,即使是不同测试用例或者不同源文件中的测试。也可以通过gtest_filter和正则匹配运行指定的测试。
  • [问题求助] 华为云如何通过BC35或者其他移远模组给单片机进行程序升级
    可参考的资料实在烧的可怜,这么重要的点,可参考资料竟然这么少。 大师们给个参考吧
  • [技术干货] 小熊派LiteOS移植LVGL
    小熊派LiteOS移植LVGL 一、移植前言 二、配置 TFT 三、LVGL 源码获取 四、显示接口移植 五、Demo 代码 六、实验现象 小熊派LiteOS移植LVGL一、移植前言之前使用小熊派实现了鸿蒙动画的开机界面,具体使用的技术栈为 STM32 + LiteOS + LVGL + FATFS +DMA 方式实现,刷新效率非常高,预览视频如下:关于这个的实现过程我会写一系列的教程分享出来,主要分为下面几个部分,本节为第二部分,基于 LiteOS 移植 LVGL 显示接口• 小熊派移植华为 LiteOS-M(基于MDK):链接 ;• 小熊派基于 LiteOS 移植 LVGL 显示接口:链接 ;• 小熊派基于 LiteOS 移植 LVGL 文件系统:链接 ;• 小熊派实现鸿蒙开机界面(LiteOS+LVGL):链接 ;本节的教程就是先通过 STM32CubeMX 来配置 小熊派的 TFT 初始化代码,开启 DMA 加速(不开启会卡出翔),配置完成后获取 LVGL 的代码,移植到工程里面,然后将 TFT 驱动接口和 LVGL 接口对接,在运行 Demo 代码 二、配置 TFT我们在上一节移植好 LiteOS 工程的基础上使用 CubeMX 配置 TFT 的 SPI 接口,具体 SPI 驱动接口可以参考这篇文章:小熊派 FreeRTOS+SPI+DMA 驱动 TFT-LCD SPI 配置完成如下:开启 DMA,并且在 NVIC 里面使能中断除了上面的 SPI 引脚还需要,配置 TFT 的其他控制引脚,关于引脚在参考文章中有写出来,配置完成如下:在 MDK 工程根目录下创建 Hardware/LCD 文件夹用来存放驱动代码,驱动文件命名为 lcd.c 和 lcd.h拷贝下面的代码进去lcd.c #include "lcd.h"#include "gpio.h"#include "spi.h"#include "cmsis_os.h"extern osSemaphoreId_t DMA_SemaphoreHandle;/* USER CODE BEGIN 1 *//*** @brief SPI 发送字节函数* @param TxData 要发送的数据* @param size 发送数据的字节大小* @return 0:写入成功,其他:写入失败*/uint8_t SPI_WriteByte(uint8_t *TxData,uint16_t size){osStatus_t result;//获取信号,如果上一个DMA传输完成//信号就能获取到,没有传输完成任务就挂起//等到传输完成再恢复result = osSemaphoreAcquire(DMA_SemaphoreHandle,0xFFFF);if(result == osOK){//获取成功return HAL_SPI_Transmit_DMA(&hspi2,TxData,size);}else{//获取失败return 1;}}//DMA 传输完成后会调用 SPI传输完成回调函数//在该函数中我们释放信号void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi){if(hspi->Instance == hspi2.Instance)osSemaphoreRelease(DMA_SemaphoreHandle);}/*** @brief 写命令到LCD* @param cmd —— 需要发送的命令* @return none*/static void LCD_Write_Cmd(uint8_t cmd){LCD_WR_RS(0);SPI_WriteByte(&cmd, 1);}/*** @brief 写数据到LCD* @param dat —— 需要发送的数据* @return none*/static void LCD_Write_Data(uint8_t dat){LCD_WR_RS(1);SPI_WriteByte(&dat, 1);}/*** @breif 打开LCD显示背光* @param none* @return none*/void LCD_DisplayOn(void){LCD_PWR(1);}/*** @brief 关闭LCD显示背光* @param none* @return none*/void LCD_DisplayOff(void){LCD_PWR(0);}/*** @brief 设置数据写入LCD显存区域* @param x1,y1 —— 起点坐标* @param x2,y2 —— 终点坐标* @return none*/void LCD_Address_Set(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2){/* 指定X方向操作区域 */LCD_Write_Cmd(0x2a);LCD_Write_Data(x1 >> 8);LCD_Write_Data(x1);LCD_Write_Data(x2 >> 8);LCD_Write_Data(x2);/* 指定Y方向操作区域 */LCD_Write_Cmd(0x2b);LCD_Write_Data(y1 >> 8);LCD_Write_Data(y1);LCD_Write_Data(y2 >> 8);LCD_Write_Data(y2);/* 发送该命令,LCD开始等待接收显存数据 */LCD_Write_Cmd(0x2C);}/*** @brief 以一种颜色清空LCD屏* @param color —— 清屏颜色(16bit)* @return none*/void LCD_Clear(uint16_t color){uint16_t i;uint8_t data[2] = {0}; //color是16bit的,每个像素点需要两个字节的显存/* 将16bit的color值分开为两个单独的字节 */data[0] = color >> 8;data[1] = color;LCD_Address_Set(0, 0, LCD_Width - 1, LCD_Height - 1);LCD_WR_RS(1);for(i=0;i<((LCD_Width)*(LCD_Height));i++){SPI_WriteByte(data, 2);}}/*** @brief LCD初始化* @param none* @return none*/void LCD_Init(void){/* 复位LCD */LCD_PWR(0);LCD_RST(0);osDelay(100);LCD_RST(1);osDelay(120);/* 关闭睡眠模式 */LCD_Write_Cmd(0x11);osDelay(120);/* 开始设置显存扫描模式,数据格式等 */LCD_Write_Cmd(0x36);LCD_Write_Data(0x00);/* RGB 5-6-5-bit格式 */LCD_Write_Cmd(0x3A);LCD_Write_Data(0x65);/* porch 设置 */LCD_Write_Cmd(0xB2);LCD_Write_Data(0x0C);LCD_Write_Data(0x0C);LCD_Write_Data(0x00);LCD_Write_Data(0x33);LCD_Write_Data(0x33);/* VGH设置 */LCD_Write_Cmd(0xB7);LCD_Write_Data(0x72);/* VCOM 设置 */LCD_Write_Cmd(0xBB);LCD_Write_Data(0x3D);/* LCM 设置 */LCD_Write_Cmd(0xC0);LCD_Write_Data(0x2C);/* VDV and VRH 设置 */LCD_Write_Cmd(0xC2);LCD_Write_Data(0x01);/* VRH 设置 */LCD_Write_Cmd(0xC3);LCD_Write_Data(0x19);/* VDV 设置 */LCD_Write_Cmd(0xC4);LCD_Write_Data(0x20);/* 普通模式下显存速率设置 60Mhz */LCD_Write_Cmd(0xC6);LCD_Write_Data(0x0F);/* 电源控制 */LCD_Write_Cmd(0xD0);LCD_Write_Data(0xA4);LCD_Write_Data(0xA1);/* 电压设置 */LCD_Write_Cmd(0xE0);LCD_Write_Data(0xD0);LCD_Write_Data(0x04);LCD_Write_Data(0x0D);LCD_Write_Data(0x11);LCD_Write_Data(0x13);LCD_Write_Data(0x2B);LCD_Write_Data(0x3F);LCD_Write_Data(0x54);LCD_Write_Data(0x4C);LCD_Write_Data(0x18);LCD_Write_Data(0x0D);LCD_Write_Data(0x0B);LCD_Write_Data(0x1F);LCD_Write_Data(0x23);/* 电压设置 */LCD_Write_Cmd(0xE1);LCD_Write_Data(0xD0);LCD_Write_Data(0x04);LCD_Write_Data(0x0C);LCD_Write_Data(0x11);LCD_Write_Data(0x13);LCD_Write_Data(0x2C);LCD_Write_Data(0x3F);LCD_Write_Data(0x44);LCD_Write_Data(0x51);LCD_Write_Data(0x2F);LCD_Write_Data(0x1F);LCD_Write_Data(0x1F);LCD_Write_Data(0x20);LCD_Write_Data(0x23);/* 显示开 */LCD_Write_Cmd(0x21);LCD_Write_Cmd(0x29);/*打开显示*/LCD_PWR(1);}lcd.h#include "main.h"#define LCD_PWR(n) (n?\HAL_GPIO_WritePin(LCD_PWR_GPIO_Port,LCD_PWR_Pin,GPIO_PIN_SET):\HAL_GPIO_WritePin(LCD_PWR_GPIO_Port,LCD_PWR_Pin,GPIO_PIN_RESET))#define LCD_WR_RS(n) (n?\HAL_GPIO_WritePin(LCD_WR_RS_GPIO_Port,LCD_WR_RS_Pin,GPIO_PIN_SET):\HAL_GPIO_WritePin(LCD_WR_RS_GPIO_Port,LCD_WR_RS_Pin,GPIO_PIN_RESET))#define LCD_RST(n) (n?\HAL_GPIO_WritePin(LCD_RST_GPIO_Port,LCD_RST_Pin,GPIO_PIN_SET):\HAL_GPIO_WritePin(LCD_RST_GPIO_Port,LCD_RST_Pin,GPIO_PIN_RESET))//LCD屏幕分辨率定义#define LCD_Width 240#define LCD_Height 240//颜色定义#define WHITE 0xFFFF //白色#define YELLOW 0xFFE0 //黄色#define BRRED 0XFC07 //棕红色#define PINK 0XF81F //粉色#define RED 0xF800 //红色#define BROWN 0XBC40 //棕色#define GRAY 0X8430 //灰色#define GBLUE 0X07FF //兰色#define GREEN 0x07E0 //绿色#define BLUE 0x001F //蓝色#define BLACK 0x0000 //黑色uint8_t SPI_WriteByte(uint8_t *TxData,uint16_t size);static void LCD_Write_Cmd(uint8_t cmd);static void LCD_Write_Data(uint8_t dat);void LCD_DisplayOn(void);void LCD_DisplayOff(void);void LCD_Address_Set(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);void LCD_Clear(uint16_t color);void LCD_Init(void);代码和文件添加完成后不要忘记添加文件路径,然后我们在主函数中创建一个用于 lcd 显示的任务,初始化 LCD 同时将屏幕初始化为蓝色osThreadId_t lcd_taskHandle;const osThreadAttr_t lcd_task_attributes = {.name = "lcd_task",.stack_size = 512 * 4,.priority = (osPriority_t) osPriorityNormal1,};void Lcd_Task(void *argument);void Lcd_Task(void *argument){LCD_Init();LCD_Clear(BLUE);while(1){osDelay(1000);}}添加 DMA 信号量osSemaphoreId_t DMA_SemaphoreHandle;const osSemaphoreAttr_t DMA_Semaphore_attributes = {.name = "DMA_Semaphore"};初始化信号和 LiteOS:/* USER CODE BEGIN 2 */osKernelInitialize();/* creation of uart_task */DMA_SemaphoreHandle = osSemaphoreNew(1, 1, &DMA_Semaphore_attributes);led_taskHandle = osThreadNew(Led_Task, NULL, &led_task_attributes);lcd_taskHandle = osThreadNew(Lcd_Task, NULL, &lcd_task_attributes);osKernelStart();/* USER CODE END 2 */编译烧写程序,观察现象,屏幕清屏为蓝色,驱动程序跑通了,可以进行下一步:三、LVGL 源码获取获取 lvgl 7.0 版本的源码git clone -b release/v7 https://github.com/lvgl/lvgl.git拉取后代码下面我们在 MDK 工程目录按照下面的格式建立文件夹APP 文件夹用来存放我们编写的 lvgl 应用代码,LVGL 文件夹用来存放 lvgl 的源码,以及接口代码然后我们将刚刚 github 下载的源码拷贝到 LVGL 中,然后把里面 lvgl\examples\porting 文件夹复制到同一目录下,改名为 lvgl_port 文件夹,同时将 lvgl\lv_conf_template.h 也复制到同一目录,并且改名为 lv_conf.h,修改结果如下:然后将 lvgl_port 下面的文件也修改名称为下面的格式:这6个文件是 lvgl 的接口文件,disp 是显示接口、fs 是文件系统接口、indev 是输入接口,下面我们在 MDK 工程里面添加文件和文件路径,添加路径如下:..\Middlewares\LVGL\APP..\Middlewares\LVGL\LVGL\lvgl_port..\Middlewares\LVGL\LVGL\lvgl\src添加文件如下:src 放的文件是下面文件夹的所有 c 文件config 放的是 lvgl 配置头文件:port 放的是 lvgl 的硬件接口文件文件添加完成后我们先配置 lvgl 下的 lv_conf.h 文件,做一些配置,不然直接编译的话会有一堆报错lv_conf.h 文件修改:修改屏幕尺寸适配小熊派:/* Maximal horizontal and vertical resolution to support by the library.*/#define LV_HOR_RES_MAX (240)#define LV_VER_RES_MAX (240)设置屏幕颜色深度,以及颜色存放格式(适配 ST7789芯片):/* Color depth:* - 1: 1 byte per pixel* - 8: RGB332* - 16: RGB565* - 32: ARGB8888*/#define LV_COLOR_DEPTH 16/* Swap the 2 bytes of RGB565 color.* Useful if the display has a 8 bit interface (e.g. SPI)*/#define LV_COLOR_16_SWAP 1设置调节界面缩放比例:/* Dot Per Inch: used to initialize default sizes.* E.g. a button with width = LV_DPI / 2 -> half inch wide* (Not so important, you can adjust it to modify default sizes and spaces)*/#define LV_DPI 60 /*[px]*/设置动态内存大小:/* Size of the memory used by `lv_mem_alloc` in bytes (>= 2kB)*/# define LV_MEM_SIZE (16U * 1024U)关闭使用 GPU:/* 1: Enable GPU interface*/#define LV_USE_GPU 0 /*Only enables `gpu_fill_cb` and `gpu_blend_cb` in the disp. drv- */#define LV_USE_GPU_STM32_DMA2D 0暂时先关闭文件系统:/* 1: Enable file system (might be required for images */#define LV_USE_FILESYSTEM 0编译一下,有一些警告..\Middlewares\LVGL\LVGL\lvgl\src\lv_draw\lv_draw_mask.c(350): warning: #111-D: statement is unreachable这些警告没有任何影响,可以把警告给屏蔽掉,切换到 C/C++选项卡,在 Misc Controls 中填入--diag_suppress=111 把它屏蔽掉如下图所示:编译后改报错就不显示了四、显示接口移植编译通过后,我们下一步就是修改显示接口了,打开 lv_port_disp.c 文件,将开头使能,包括头文件也使能:/*Copy this file as "lv_port_disp.c" and set this value to "1" to enable content*/#if 1修改显示接口,主要关注 void lv_port_disp_init(void) 函数void lv_port_disp_init(void){/*-------------------------* Initialize your display* -----------------------*/disp_init();/*-----------------------------* Create a buffer for drawing*----------------------------*//* LVGL requires a buffer where it internally draws the widgets.* Later this buffer will passed your display drivers `flush_cb` to copy its content to your display.* The buffer has to be greater than 1 display row** There are three buffering configurations:* 1. Create ONE buffer with some rows:* LVGL will draw the display's content here and writes it to your display** 2. Create TWO buffer with some rows:* LVGL will draw the display's content to a buffer and writes it your display.* You should use DMA to write the buffer's content to the display.* It will enable LVGL to draw the next part of the screen to the other buffer while* the data is being sent form the first buffer. It makes rendering and flushing parallel.** 3. Create TWO screen-sized buffer:* Similar to 2) but the buffer have to be screen sized. When LVGL is ready it will give the* whole frame to display. This way you only need to change the frame buffer's address instead of* copying the pixels.* *//* Example for 1) */static lv_disp_buf_t draw_buf_dsc_1;static lv_color_t draw_buf_1[LV_HOR_RES_MAX * 10]; /*A buffer for 10 rows*/lv_disp_buf_init(&draw_buf_dsc_1, draw_buf_1, NULL, LV_HOR_RES_MAX * 10); /*Initialize the display buffer*//* Example for 2) */static lv_disp_buf_t draw_buf_dsc_2;static lv_color_t draw_buf_2_1[LV_HOR_RES_MAX * 10]; /*A buffer for 10 rows*/static lv_color_t draw_buf_2_2[LV_HOR_RES_MAX * 10]; /*An other buffer for 10 rows*/lv_disp_buf_init(&draw_buf_dsc_2, draw_buf_2_1, draw_buf_2_2, LV_HOR_RES_MAX * 10); /*Initialize the display buffer*//* Example for 3) */static lv_disp_buf_t draw_buf_dsc_3;static lv_color_t draw_buf_3_1[LV_HOR_RES_MAX * LV_VER_RES_MAX]; /*A screen sized buffer*/static lv_color_t draw_buf_3_2[LV_HOR_RES_MAX * LV_VER_RES_MAX]; /*An other screen sized buffer*/lv_disp_buf_init(&draw_buf_dsc_3, draw_buf_3_1, draw_buf_3_2, LV_HOR_RES_MAX * LV_VER_RES_MAX); /*Initialize the display buffer*//*-----------------------------------* Register the display in LVGL*----------------------------------*/lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/lv_disp_drv_init(&disp_drv); /*Basic initialization*//*Set up the functions to access to your display*//*Set the resolution of the display*/disp_drv.hor_res = 480;disp_drv.ver_res = 320;/*Used to copy the buffer's content to the display*/disp_drv.flush_cb = disp_flush;/*Set a display buffer*/disp_drv.buffer = &draw_buf_dsc_1;#if LV_USE_GPU/*Optionally add functions to access the GPU. (Only in buffered mode, LV_VDB_SIZE != 0)*//*Blend two color array using opacity*/disp_drv.gpu_blend_cb = gpu_blend;/*Fill a memory array with a color*/disp_drv.gpu_fill_cb = gpu_fill;#endif/*Finally register the driver*/lv_disp_drv_register(&disp_drv);}disp_init() 用来初始化显示屏外设,这里我们在hal初始化中已经初始化完成了,所以删除他下面的代码就是创建一个缓存 buffer,这里 LVGL 提供了三种方式创建缓存:第一种只创建一个缓存区,长度是横轴像素长度的 10 倍,第二种创建两个缓存区,长度都是 横轴的 10 倍,第三种则是创建两个,大小是横轴乘以纵轴,相当于整个屏幕大小,第一种情况,如果我们在写入数据时不能修改,第二种我们在写入一个 buffer 时还可以希尔另外一个 buffer ,可以结合 DMA 加快写入速度,这里我使用第一种下面的代码注册显示驱动,配置其参数:主要就是配置屏幕参数,设置刷新函数,配置缓存区指针,最后注册驱动,这里我们要修改一下刷新屏幕函数static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)修改如下:/* Flush the content of the internal buffer the specific area on the display* You can use DMA or any hardware acceleration to do this operation in the background but* 'lv_disp_flush_ready()' has to be called when finished. */static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p){/*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/int32_t y;LCD_Address_Set(area->x1,area->y1,area->x2,area->y2);LCD_WR_RS(1);//一行一行 DMAfor(y = area->y1; y <= area->y2; y++) {if(osSemaphoreAcquire(DMA_SemaphoreHandle,0xFFFF) == osOK)HAL_SPI_Transmit_DMA(&hspi2,(uint8_t *)color_p,(uint16_t)(area->x2-area->x1+1)*2);color_p += (area->x2-area->x1+1);}/* IMPORTANT!!!* Inform the graphics library that you are ready with the flushing*/lv_disp_flush_ready(disp_drv);}修改代码后,要添加头文件和 dma 信号量声明/********************** INCLUDES*********************/#include "lv_port_disp.h"#include "lcd.h"#include "spi.h"#include "cmsis_os.h"/********************** DEFINES*********************/extern osSemaphoreId_t DMA_SemaphoreHandle;五、Demo 代码在定时器 1 中断中添加 lvgl 的时基更新代码void TIM1_UP_TIM16_IRQHandler(void){/* USER CODE BEGIN TIM1_UP_TIM16_IRQn 0 *//* USER CODE END TIM1_UP_TIM16_IRQn 0 */HAL_TIM_IRQHandler(&htim1);/* USER CODE BEGIN TIM1_UP_TIM16_IRQn 1 */lv_tick_inc(1);/* USER CODE END TIM1_UP_TIM16_IRQn 1 */}在 main.c 的 lcd 任务中添加创建 label 测试的代码这里的测试代码是画两个对角位置的方块,边框颜色都不一样,一个设置的是蓝色,一个是绿色void Lcd_Task(void *argument){LCD_Init();lv_init();lv_port_disp_init();//lvgl 显示接口初始化,放在 lv_init()的后面lv_style_t style1;lv_style_init(&style1);lv_style_set_bg_color(&style1, LV_STATE_DEFAULT,LV_COLOR_BLACK);lv_style_set_border_width(&style1,LV_STATE_DEFAULT, 5);lv_style_set_border_color(&style1,LV_STATE_DEFAULT, LV_COLOR_BLUE);lv_style_t style2;lv_style_init(&style2);lv_style_set_bg_color(&style2, LV_STATE_DEFAULT,LV_COLOR_BLACK);lv_style_set_border_width(&style2,LV_STATE_DEFAULT, 5);lv_style_set_border_color(&style2,LV_STATE_DEFAULT, LV_COLOR_GREEN);lv_obj_t* bgk1 = lv_obj_create(lv_scr_act(), NULL);//创建对象lv_obj_set_pos(bgk1,0,0);lv_obj_set_size(bgk1, 120, 120);//设置覆盖大小lv_obj_add_style(bgk1,LV_STATE_DEFAULT, &style1);lv_obj_t* bgk2 = lv_obj_create(lv_scr_act(), NULL);//创建对象lv_obj_set_pos(bgk2,120,120);lv_obj_set_size(bgk2, 120, 120);//设置覆盖大小 lv_obj_add_style(bgk2,LV_STATE_DEFAULT, &style2);while(1){lv_task_handler();osDelay(1000);}}/* USER CODE END 0 */记得添加相关头文件:/* USER CODE BEGIN Includes */#include "cmsis_os.h"#include "lcd.h"#include "lv_port_disp.h"/* USER CODE END Includes */编译下载代码六、实验现象两个对角小方块,边框一个蓝色一个绿色
  • [技术干货] 小熊派低功耗串口接受数据异常
    低功耗串口接受数据异常 一、异常信息 1.1 硬件环境 1.2 软件环境 1.3 问题描述 二、问题分析 三、解决方式 小熊派低功耗串口接受数据异常一、异常信息1.1 硬件环境小熊派 STM32L4 单片机1.2 软件环境STM32CubeMX 6.2.1MDK 5.31.3 问题描述使用 STM32CubeMX 配置 STM32 的 LPUART 后,生成代码使用串口 DMA 接受数据,在开启数据接受后,指定接受的数据长度,接受完成后,串口继续接受数据,在开启下一个串口接受时则会读取之前的数据,例如:串口使能接受 5 个数据,然后上位机发送 ABCDEFG,本次串口 DMA 在接受到 ABCDE 时就会产生接受完成标志,调用回调函数,至于 FG 则不会接受丢弃掉,但我如果在设置接受 2 个数据,这两个数据居然还能接受到在仿真调试里面运行程序时这两个数据不会接受到,很正常,符合逻辑,但一旦实际下载到单片机里面实际运行就会出现错误二、问题分析根据实验现象,仿真是很正常的,但实际下载运行就会出现问题,推测编译器或者生成的代码有问题理论上第一次接受数量达标后,代码会将接受完成标志置位,然后串口的缓存区不会再接受任何数据,出现这种情况的原因可能是接受完成后数据 DMA 和串口没有关闭三、解决方式在调用之前我们将 DMA 的缓存区清空即可,例如我下面的代码,使用 memset 清空缓存区
  • [交流分享] 聊聊万物互联-Wi-Fi6
    WiFi的历史从802.11的FHFS,DSSS到802.11b的DSSS,到802.11a的OFDM,802.11g的ERP(将OFDM从5G迁移到了2.4G),到802.11n的更宽频带(40MHz)的OFDM技术,到802.11ac的进一步拓宽(80, 80+80,160MHz)的OFDM技术,到802.11ax的更窄的子载波(78.125kHz)的OFDM技术,到讨论中的802.11be的320MHz的OFDM技术。OFDM技术从802.11a,大概2001年的时候,到现在,已经持续演进了20年了,它的抗多径效应,适合无线空间的复杂环境。因为802.11ax(Wi-FI 6)希望覆盖更广的空间,所以把载波宽度进一步变小了。每一代的发展,频带基本是越来越宽,似乎是可以无限的把频宽扩展下去。但是这应该是存在问题的,多样化的需求下,大频宽是可能浪费频段的。毕竟有些地方只需要小的频宽就好了。802.11ax还定义了一个仅支持20MHz的模式,也是瞄准了万物互联的趋势下,小数据,低能耗的搭配。但是802.11be(TB Wi-Fi 7)又把带宽变得更大了,每一代总是希望能更快的。但是单纯更快有什么意思呢。OFDM不止可以分频带,因为网络的特性,还可以分用户,这是Wi-Fi现在越来越看重的,希望越来越多的用户都是使用这个网络,而且还要能用。从802.11ac(2013)开始的Wi-Fi5已经引入了多用户的观念,对应的技术是MU-MIMO。它是这么样的,不同的用户用的是不同的天线。到了802.1ax(Wi-Fi6),支持上行的MU-MIMO,而且还把一个频段同时分配给了多个用户(OFDMA),每个用户最少可以只用2MHz的带宽。一下还把上行和下行都给加进去了。不止如此,还加入了BSS Coloring技术,弱化Exposed Node的问题,提升密集网络覆盖下的并行性。Wi-Fi6有这么多优势,其实它的主要着笔点在于密集用户,密集网络。想想现在Wi-Fi6的设备已经都出来1两年了,但是也并没有多改变生态。其实现在大家也觉得够用了,就像我自己,这种Wi-Fi6的理念对人这个用户来讲真的是有点超前了,哪里去找如此密集的人流和网络?办公场所,体育场?这些场合毕竟是少数。自动化生产车间也许是比较合适的,不过不是人流,而是物流。Wi-Fi7似乎要在这个道路上越走越远,越来越远…
  • [Atlas 200] linux spi驱动分析-1
    # 准备   使用的源码包为华为官方的ascend200AI加速模块的SDK,其下载地址位于:[点击跳转](https://www.hiascend.com/hardware/firmware-drivers?tag=community)   使用的固件与驱动版本为:1.0.9.alpha   压缩包名称为:A200-3000-sdk_20.2.0.zip   将A200-3000-sdk_20.2.0.zip解压后可以看到Ascend310-source-minirc.tar.gz压缩包,这个压缩包里有ascend200AI加速模块的linux内核源码包、设备树及驱动文件等。 # 设备树节点和驱动的匹配   这里不做过多的赘述了,可自行百度。   其spi的设备树节点位于source/dtb/hi1910-fpga-spi.dtsi中,内容如下: ```bash alg_clk: alg_clk { compatible = "fixed-clock"; #clock-cells = 0>; clock-frequency = 200000000>; }; spi_0: spi@130980000{ #address-cells = 1>; #size-cells = 0>; compatible = "hisi-spi"; reg = 0x1 0x30980000 0 0x10000>, 0x1 0x30900000 0 0x1000>; interrupts = 0 322 4>; clocks = &alg_clk>; clock-names = "spi_clk"; num-cs = 2>; id = 0>; status = "ok"; }; ```   spi_0和spi_1节点的compatible 属性会和spi控制器的驱动匹配。(位于source/drivers/dev_plat/spi/spi-hisi.c) ```bash STATIC const struct of_device_id hisi_spi_dt_ids[] = { { .compatible = "hisi-spi" }, { /* sentinel */ } }; ``` ```bash STATIC struct platform_driver hisi_spi_driver = { .driver = { .name = "hisi_spi", .pm = HISI_SPI_PM_OPS, .of_match_table = of_match_ptr(hisi_spi_dt_ids), #ifdef CONFIG_ACPI .acpi_match_table = ACPI_PTR(hisi_spi_acpi_ids), #endif }, .probe = hisi_spi_probe, .remove = hisi_spi_remove, }; ```   内核会先解析设备树节点,然后驱动程序注册的时候of_match_ptr中的of_device_id会和设备树节点进行匹配。 # spi控制器驱动分析(source/dev_plat/spi/spi-hisi.c)   spi_0和spi_1的设备树节点都会匹配上spi控制器驱动,然后执行他的probe函数,由于spi_1节点的状态为disable,所以只有spi_0节点会执行一次probe函数 ```bash int ret; int irq; u32 id = 0; u32 cs_num = 0; u32 rate = 0; struct resource *res = NULL; struct resource *res_iomux = NULL; struct hisi_spi *hs = NULL; struct spi_master *master = NULL; ```   初始化变量 ```bash ASSERT_RET((pdev != NULL), -EINVAL); dev_info(&pdev->dev, "hi_spi_probe enter..\n"); dev_info(&pdev->dev, "hi:dev name %s, id %d, auto %d, num res %u\n", pdev->name, pdev->id, pdev->id_auto, pdev->num_resources); ```   ASSERT_RET断言,判断platform_device *pdev结构体是否为空。 dev_info和printk类似,在内核启动时打印消息 ```c ret = hisi_spi_get_dts(pdev, &cs_num, &id, &rate); if (ret != 0) { dev_err(&pdev->dev, "hisi_spi_get_dts fail\n"); return -EINVAL; } ```   hisi_spi_get_dts用于获取片选和时钟等信息 ```c STATIC int hisi_spi_get_dts(struct platform_device *pdev, u32 *cs_num, u32 *id, u32 *rate) { int ret; #ifndef CONFIG_ACPI struct clk *clk = NULL; #endif if ((pdev == NULL) || (cs_num == NULL) || (id == NULL) || (rate == NULL)) { return -EINVAL; } #ifdef CONFIG_ACPI ret = device_property_read_u32(&pdev->dev, "num-cs", cs_num); if (ret 0) { dev_err(&pdev->dev, "get num_cs error: %d \n", ret); return -EINVAL; } ret = device_property_read_u32(&pdev->dev, "id", id); if (ret 0) { dev_err(&pdev->dev, "get id error: %d \n", ret); return -EINVAL; } ret = device_property_read_u32(&pdev->dev, "spi_clk", rate); if (ret 0) { dev_err(&pdev->dev, "get clk error : %d\n", ret); return -EINVAL; } #else ret = of_property_read_u32(pdev->dev.of_node, "num-cs", cs_num); if (ret != 0) { dev_err(&pdev->dev, "of_property_read_u32 get num-cs fail\n"); return -EINVAL; } ret = of_property_read_u32(pdev->dev.of_node, "id", id); if (ret != 0) { dev_err(&pdev->dev, "of_property_read_u32 get id fail\n"); return -EINVAL; } clk = clk_get(&pdev->dev, "spi_clk"); if (IS_ERR(clk)) { return PTR_ERR(clk); } *rate = clk_get_rate(clk); clk_put(clk); #endif return 0; } ```   这个ACPI好像和电源管理有关,内核中全局搜索CONFIG_ACPI也未找到定义,所里这里先不管他。   这个hisi_spi_get_dts会获取设备书中spi节点信息   片选数量cs_num=2   spi节点id=0   spi时钟速率rate = 200000000   接着获取设备树描述中的mem资源 ```c /* get base addr */ res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (unlikely(res == NULL)) { dev_err(&pdev->dev, "invalid resource\n"); return -EINVAL; } ```   这里要先说一下platform_get_resource这个函数,内核函数定义如下: ```c struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num) { int i; for (i = 0; i dev->num_resources; i++) { struct resource *r = &dev->resource[i]; if (type == resource_type(r) && num-- == 0) return r; } return NULL; } ```   从第一份资源开始匹配,type用来匹配资源类型num--==0表示你想获得的是第多少份资源   spi0设备树节点: ```c reg = 0x1 0x30980000 0 0x10000>, 0x1 0x30900000 0 0x1000>; interrupts = 0 322 4>; ```   则spi0的pdev结构体的资源有三个,两个为IORESOURCE_MEM,一个为IORESOURCE_IRQ。这里调用: ```bash res = platform_get_resource(pdev, IORESOURCE_MEM, 0); ```   获取的是0x1 0x30980000 0 0x10000>他表示spi控制器的内存起始地址为0x30980000 ,大小为0x10000。然后获取irq资源: ```c irq = platform_get_irq(pdev, 0); if (irq 0) { dev_err(&pdev->dev, "platform_get_irq error\n"); return -ENODEV; } dev_info(&pdev->dev, "hi: get irq %d\n", irq); ```   中断代码中我没有使用过,所以不管他了,接下来是分配spi_master结构体的内存: ```c master = spi_alloc_master(&pdev->dev, sizeof(*hs)); if (master == NULL) { dev_err(&pdev->dev, "spi_alloc_master error.\n"); return -ENOMEM; } ```   跳入spi_alloc_master里面: ```c static inline struct spi_controller *spi_alloc_master(struct device *host, unsigned int size) { return __spi_alloc_controller(host, size, false); } ``` ```c struct spi_controller *__spi_alloc_controller(struct device *dev, unsigned int size, bool slave) { struct spi_controller *ctlr; if (!dev) return NULL; ctlr = kzalloc(size + sizeof(*ctlr), GFP_KERNEL); if (!ctlr) return NULL; device_initialize(&ctlr->dev); ctlr->bus_num = -1; ctlr->num_chipselect = 1; ctlr->slave = slave; if (IS_ENABLED(CONFIG_SPI_SLAVE) && slave) ctlr->dev.class = &spi_slave_class; else ctlr->dev.class = &spi_master_class; ctlr->dev.parent = dev; pm_suspend_ignore_children(&ctlr->dev, true); spi_controller_set_devdata(ctlr, &ctlr[1]); return ctlr; } EXPORT_SYMBOL_GPL(__spi_alloc_controller); ```   重点关注这两行: ```c ctlr = kzalloc(size + sizeof(*ctlr), GFP_KERNEL); spi_controller_set_devdata(ctlr, &ctlr[1]); ```   总的来说这个spi_alloc_master函数初始化了spi_controller结构体的dev和各个参数,将其设置为master而不是slave,分配的是spi_controller结构体加上自定义的hisi_spi结构体大小,其中master->dev->driver_data指针指向后面这片大小为hisi_spi结构体的内存,hisi_spi是各个厂家根据自己的控制器驱动自定义的结构体。然后接下来填充spi_master结构体 ```c /* the spi->mode bits understood by this driver: */ master->mode_bits = SPI_CPHA | SPI_CPOL; master->dev.of_node = pdev->dev.of_node; master->bus_num = pdev->id; master->num_chipselect = cs_num; master->setup = hisi_spi_setup; master->transfer_one_message = hisi_spi_transfer_one_message; master->cleanup = hisi_spi_cleanup; ```   spi模式默认使用SPI_MODE_3,即clock信号默认为高电平,data信号在clock的第二个跳变沿解析数据。我们使用的是spi0,他的id为0,所以这里的bus_num为0,num_chipselect 为2   hisi_spi_transfer_one_message这个函数,这是spi收发数据的函数。具体在下面讲。 ```c platform_set_drvdata(pdev, master); ```   master是经过上面的spi_alloc_master函数malloc出来的地址,经过platform_set_drvdata后pdev->dev->driver_data指向这片地址,需要的时候可以使用platform_get_drvdata在拿出来使用 ```c hs = spi_master_get_devdata(master); ASSERT_RET((hs != NULL), -ENODEV); ```   spi_master_get_devdata这个函数就是我们上面提到过的,spi_alloc_master分配的除了spi_controller 以外的大小为hisi_spi结构体的大小。 ```c spin_lock_init(&hs->lock); hs->flags = 0; hs->phybase = res->start; hs->pdev = pdev; hs->spi_id = id; hs->clk_rate = (unsigned long)rate; hs->n_bytes = 1; hs->dfs = 8; hs->regs = ioremap(res->start, resource_size(res)); if (hs->regs == NULL) { dev_err(&pdev->dev, "ioremap error.\n"); ret = -ENOMEM; goto out_ioremap; } hs->irq = irq; ret = request_irq(irq, hisi_spi_interrupt, 0, dev_name(&pdev->dev), master); if (ret) { dev_err(&pdev->dev, "request_irq error %d.\n", ret); goto out_irq; } dev_info(&pdev->dev, "hi: get clk rate %d\n", rate); ```   填充hisi_spi结构体,初始化自旋锁,设备clk_rate,和寄存器地址等内容。 ```c res_iomux = platform_get_resource(pdev, IORESOURCE_MEM, 1); if (res_iomux != NULL) { ret = hisi_spi_iomux_init(hs, res_iomux->start); if (ret) { dev_err(&pdev->dev, "hi_spi_iomux_init error %d.\n", ret); goto out_irq; } } ```   res_iomux获取的是第2个IORESOURCE_MEM资源,即设备树中的0x1 0x30900000 0 0x1000>,然后执行hisi_spi_iomux_init函数来初始化spi的复用功能 ```c STATIC int hisi_spi_iomux_init(struct hisi_spi *hs, unsigned long addr) { void __iomem *regs = NULL; regs = ioremap(addr, IO_MUX_MEM_REMAP_SIZE); if (regs == NULL) { dev_err(&hs->pdev->dev, "remap mem failed.\n"); return -EBUSY; } if (hs->spi_id == 0) { writel_relaxed(FUNC_CTRL_REG_SEL_DS1, regs + SPI1_CLK_FUNC_CTRL_REG); } else if (hs->spi_id == 1) { writel_relaxed(FUNC_CTRL_REG_SEL_DS1, regs + SPI2_CLK_FUNC_CTRL_REG); } else if (hs->spi_id == 2) { writel_relaxed(IOMUX_REG_SEL_SPI2, regs + IOMUX_REG_SPI2_CLK); writel_relaxed(IOMUX_REG_SEL_SPI2, regs + IOMUX_REG_SPI2_CSN); writel_relaxed(IOMUX_REG_SEL_SPI2, regs + IOMUX_REG_SPI2_MOSI); writel_relaxed(IOMUX_REG_SEL_SPI2, regs + IOMUX_REG_SPI2_MISO); writel_relaxed(FUNC_CTRL_REG_SEL_DS2 | FUNC_CTRL_REG_SEL_ST, regs + SCL2_FUNC_CTRL_REG); writel_relaxed(FUNC_CTRL_REG_SEL_DS0 | FUNC_CTRL_REG_SEL_ST | FUNC_CTRL_REG_SEL_RESERVE1 | FUNC_CTRL_REG_SEL_PD | FUNC_CTRL_REG_SEL_PU, regs + SDA2_FUNC_CTRL_REG); writel_relaxed(FUNC_CTRL_REG_SEL_DS0 | FUNC_CTRL_REG_SEL_ST | FUNC_CTRL_REG_SEL_RESERVE1 | FUNC_CTRL_REG_SEL_PD | FUNC_CTRL_REG_SEL_PU, regs + UART1_RXD_FUNC_CTRL_REG); writel_relaxed(FUNC_CTRL_REG_SEL_DS1, regs + UART1_TXD_FUNC_CTRL_REG); } else { dev_err(&hs->pdev->dev, "invalid spi id %d\n", hs->spi_id); } iounmap(regs); regs = NULL; return 0; } ```   由于我们spi_1是disable,只使用了spi_0,所以这个函数主要是执行了下面这一句 ```c writel_relaxed(FUNC_CTRL_REG_SEL_DS1, regs + SPI1_CLK_FUNC_CTRL_REG); //spi-hisi.h #define FUNC_CTRL_REG_SEL_DS1 (1 5) #define SPI1_CLK_FUNC_CTRL_REG 0x814 ```   就是把SPI1_CLK_FUNC_CTRL_REG 寄存器的第五位置了1,这里应该是他的复用功能寄存器复用为了spi。 ```c ret = hisi_spi_hw_init(hs); if (ret) { dev_err(&pdev->dev, "hisi_spi_hw_init error %d.\n", ret); goto out_spi; } ``` ```c STATIC int hisi_spi_hw_init(struct hisi_spi *hs) { u32 val; hisi_spi_disable_chip(hs); /* RX_SAMPLE_DLYΪ2 */ spi_writel(hs, RX_SAMPLE_DLY, RXD_DELAY_CYCLE); spi_writel(hs, TXFTLR, 63); spi_writel(hs, RXFTLR, 63); val = 0x3a; spi_writel(hs, IMR, val); hisi_spi_enable_chip(hs); return 0; } ```   然后是初始化spi控制器的寄存器,SPI_SSIENR(0x0008)好像是他所有的spi控制器的使能寄存器,初始化之前要写0disable,完成后在写1enable。然后对输入采样的延时周期设置,然后是对发送和接收fifo的设置,都设置为63个字节。(怪不得当时使用的时候发现每次传输超过64个字节cs就自动拉高了,用示波器点了好久,原来问题在这里,这里可能和他们硬件有关,fpga写外设时序的时候就给发送和接受缓冲区分配了63个字节),然后是设置中断屏蔽寄存器,也先不管他。 ```c ret = spi_register_master(master); if (ret) { dev_err(&pdev->dev, "spi_register_master error %d.\n", ret); goto out_spi; } ```   spi_register_master这个函数我没有看很明白,但是他是spi控制器驱动必须的,好像只要功能是向linux内核注册这个控制器,让设备驱动能找到这个控制器,经过这个函数后就能在/sys/bus/spi/device目录下找到设备以及他的子节点了。我们可以跳进去简单的看一下 ```c int spi_register_controller(struct spi_controller *ctlr) { 。。。 。。。 /* register the device, then userspace will see it. * registration fails if the bus ID is in use. */ dev_set_name(&ctlr->dev, "spi%u", ctlr->bus_num); status = device_add(&ctlr->dev); if (status 0) { /* free bus id */ mutex_lock(&board_lock); idr_remove(&spi_master_idr, ctlr->bus_num); mutex_unlock(&board_lock); goto done; } dev_dbg(dev, "registered %s %s\n", spi_controller_is_slave(ctlr) ? "slave" : "master", dev_name(&ctlr->dev)); /* * If we're using a queued driver, start the queue. Note that we don't * need the queueing logic if the driver is only supporting high-level * memory operations. */ if (ctlr->transfer) { dev_info(dev, "controller is unqueued, this is deprecated\n"); } else if (ctlr->transfer_one || ctlr->transfer_one_message) { status = spi_controller_initialize_queue(ctlr); if (status) { device_del(&ctlr->dev); /* free bus id */ mutex_lock(&board_lock); idr_remove(&spi_master_idr, ctlr->bus_num); mutex_unlock(&board_lock); goto done; } } /* add statistics */ spin_lock_init(&ctlr->statistics.lock); mutex_lock(&board_lock); list_add_tail(&ctlr->list, &spi_controller_list); list_for_each_entry(bi, &board_list, list) spi_match_controller_to_boardinfo(ctlr, &bi->board_info); mutex_unlock(&board_lock); /* Register devices from the device tree and ACPI */ of_register_spi_devices(ctlr); acpi_register_spi_devices(ctlr); done: return status; } ```   首先spi_controller_check_ops、spi_controller_is_slave检查传入的spi_controller 结构体是否设置正确,是否设置transfer、transfer_one_message等函数,重点关注下spi_controller_initialize_queue和of_register_spi_devices这两个函数: ```c static int spi_controller_initialize_queue(struct spi_controller *ctlr) { int ret; ctlr->transfer = spi_queued_transfer; if (!ctlr->transfer_one_message) ctlr->transfer_one_message = spi_transfer_one_message; /* Initialize and start queue */ ret = spi_init_queue(ctlr); if (ret) { dev_err(&ctlr->dev, "problem initializing queue\n"); goto err_init_queue; } ctlr->queued = true; ret = spi_start_queue(ctlr); if (ret) { dev_err(&ctlr->dev, "problem starting queue\n"); goto err_start_queue; } return 0; err_start_queue: spi_destroy_queue(ctlr); err_init_queue: return ret; } ```   将spi_controller的transfer 函数设置为spi_queued_transfer,下面在一开始的probe将transfer_one_message设置为了hisi_spi_transfer_one_message,所以不用管他。spi_init_queue函数初始化并且建立了一个工作线程,并且给该线程指定了一个工作函数spi_pump_messages,spi_start_queue将线程工作标注running置为true。   在spi的设备驱动中会调用master->transfer,则spi_queued_transfer函数会被调用,spi_queued_transfer将设备驱动封装的spi_message加入到master的queue链表中,然后唤醒工作函数,执行spi_pump_messages,在该函数中会执行: ```c ret = ctlr->transfer_one_message(ctlr, ctlr->cur_msg); ```   即调用了前面设置过的hisi_spi_transfer_one_message函数进行数据传输。 ```c //spi_register_master->spi_register_controller->of_register_spi_devices static void of_register_spi_devices(struct spi_controller *ctlr) { struct spi_device *spi; struct device_node *nc; if (!ctlr->dev.of_node) return; for_each_available_child_of_node(ctlr->dev.of_node, nc) { if (of_node_test_and_set_flag(nc, OF_POPULATED)) continue; spi = of_register_spi_device(ctlr, nc); if (IS_ERR(spi)) { dev_warn(&ctlr->dev, "Failed to create SPI device for %pOF\n", nc); of_node_clear_flag(nc, OF_POPULATED); } } } #else static void of_register_spi_devices(struct spi_controller *ctlr) { } #endif ```   这个for_each_available_child_of_node函数我跳进去大概看了一下。 ```c #define for_each_available_child_of_node(parent, child) \ for (child = of_get_next_available_child(parent, NULL); child != NULL; \ child = of_get_next_available_child(parent, child)) struct device_node *of_get_next_available_child(const struct device_node *node, struct device_node *prev) { struct device_node *next; unsigned long flags; if (!node) return NULL; raw_spin_lock_irqsave(&devtree_lock, flags); next = prev ? prev->sibling : node->child; for (; next; next = next->sibling) { if (!__of_device_is_available(next)) continue; if (of_node_get(next)) break; } of_node_put(prev); raw_spin_unlock_irqrestore(&devtree_lock, flags); return next; } EXPORT_SYMBOL(of_get_next_available_child); static bool __of_device_is_available(const struct device_node *device) { const char *status; int statlen; if (!device) return false; status = __of_get_property(device, "status", &statlen); if (status == NULL) return true; if (statlen > 0) { if (!strcmp(status, "okay") || !strcmp(status, "ok")) return true; } return false; } ```   parent代表的是当前节点,child = of_get_next_available_child(parent, NULL);表示的是第一个子节点,child = of_get_next_available_child(parent, child)表示的是当前子节点的下一个子节点,直到子节点为NULL。__of_device_is_available的意思是如果status属性为空则返回true,即默认该节点是开启的,如果有status,且他的属性是okay或者ok,则也返回ture。否则返回false。那么最上面的for_each_available_child_of_node意思就好理解了。会遍历spi节点下能用的子节点,然后执行of_register_spi_device ```c static struct spi_device * of_register_spi_device(struct spi_controller *ctlr, struct device_node *nc) { struct spi_device *spi; int rc; /* Alloc an spi_device */ spi = spi_alloc_device(ctlr); if (!spi) { dev_err(&ctlr->dev, "spi_device alloc error for %pOF\n", nc); rc = -ENOMEM; goto err_out; } /* Select device driver */ rc = of_modalias_node(nc, spi->modalias, sizeof(spi->modalias)); if (rc 0) { dev_err(&ctlr->dev, "cannot find modalias for %pOF\n", nc); goto err_out; } rc = of_spi_parse_dt(ctlr, spi, nc); if (rc) goto err_out; /* Store a pointer to the node in the device structure */ of_node_get(nc); spi->dev.of_node = nc; /* Register the new device */ rc = spi_add_device(spi); if (rc) { dev_err(&ctlr->dev, "spi_device register error %pOF\n", nc); goto err_of_node_put; } return spi; err_of_node_put: of_node_put(nc); err_out: spi_dev_put(spi); return ERR_PTR(rc); } ```   spi_alloc_device在内核中申请spi_device的内存,of_modalias_node函数将子节点的compatible属性strlcpy 给spi->modalias,of_spi_parse_dt这个函数里面有地方需要关注下: ```c /* Device address */ rc = of_property_read_u32(nc, "reg", &value); if (rc) { dev_err(&ctlr->dev, "%pOF has no valid 'reg' property (%d)\n", nc, rc); return rc; } spi->chip_select = value; /* Device speed */ rc = of_property_read_u32(nc, "spi-max-frequency", &value); if (rc) { dev_err(&ctlr->dev, "%pOF has no valid 'spi-max-frequency' property (%d)\n", nc, rc); return rc; } spi->max_speed_hz = value; ```   spi_device结构体中的spi->chip_select的值,是spi设备树子节点的reg属性,即reg代表片选,这个地方在设备驱动生成设备的时候会用到。总而言之,会生成两个spi_device设备挂载在spi总线上。   后面的就都是一些收尾工作了。hisi_spi_transfer_one_message是发送和接收的函数,我看不下去了,里面的一些发送接收时序和具体的寄存器配置有兴趣自己看一下吧,但是应该只有厂家搞得懂。