ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 基于RT-Thread之HAPH数据采集仪设计 ## [toc] HAPH数据采集仪初始版本于2017年完成,MCU为STM32F103RCT6,显示为SPI接口的OLED屏,程序是在原数据采集器(蓝盒)的基础上修改的,裸奔,没有跑RTOS。重新设计的目标是采用操作系统RT-Thread,实现的基本功能如下: * Modbus RTU从站 * OLED显示、按键输入 * 无线通讯功能,用于主机访问HAPH数据采集仪modbus寄存器 * 继电器控制 * ADC数据采集 * SPI Flash存储及文件管理 ### 1. RT-Thread下载和开发环境的配置 ### #### 1.1 准备工作 #### * 从[RT-Thread官方代码仓库](https://github.com/RT-Thread/rt-thread)下载RT-Thread源代码,我现在下载的版本是3.0.4,可以根据需要切换至相应的版本: ``` git clone https://github.com/RT-Thread/rt-thread ``` * 下载安装[RT-Thread Env工具](https://pan.baidu.com/s/1cg28rk#list/path=%2F),配置右键打开。 * 代码编程规范参考rt_thread项目的*代码风格文档*(```documentation/coding_style_cn.txt```) #### 1.2 选择bsp #### 数据采集仪的MCU为STM32F103RCT6(*LQFP64,256Kflash,48K ram*)。RT-Thread源代码对应BSP包为*stm32f10x*和*stm32f10x-HAL*, |驱动模块|stm32f10x|stm32f10x-HAL| |:-|:-|:- |库支持|支持标准库|支持新的HAL库| CAN总线|√|×| 以太网(dma9000a)|√|× 24C64|√|× GPIO|√|√ ili LCD|√|× LED|√|× SD卡|√|√ SSD1289|√|× I2C|√|× RTC|√|× 看门狗|√|× 触摸屏|√|× 串口|√|√ SPI|×|√ USB|×|√ 暂时想不到什么理由,就先选择HAL库再说,喜新厌旧?选择挑战! 选择**stm32f10x-HAL**!!! 详见[STM32F10x-HAL 板级支持包](https://www.rt-thread.org/document/site/rt-thread/bsp/stm32f10x-HAL/README/) #### 1.3 构建MDK5工程 #### 1. 打开**RT-Thread Configuration**(输入```menuconfig```),选择相关组件和驱动; 2. 生成MDK5工程(输入```scons --help```查看命令,```scons --target=mdk5 -s```); 3. 用MDK5打开工程,选择MCU和JTAG仿真器型号,编译下载调试; #### 1.4 工程里的main()函数 #### RT-Thread里的main()函数其实是用户线程的入口,本身也是一个线程,不是传统意义里的main函数,的由来: ``` //main()相关的选项开关 #define RT_USING_COMPONENTS_INIT #define RT_USING_USER_MAIN #define RT_MAIN_THREAD_STACK_SIZE 4096//2048 //main()任务的创建 #ifdef RT_USING_HEAP tid = rt_thread_create("main", main_thread_entry, RT_NULL, RT_MAIN_THREAD_STACK_SIZE, RT_THREAD_PRIORITY_MAX / 3, 20); RT_ASSERT(tid != RT_NULL); #else ``` #### 1.5 组件和驱动启动顺序 #### 在*component.c*看到一段有关组件和驱动启动顺序的说明,如下 ``` #ifdef RT_USING_COMPONENTS_INIT /* * Components Initialization will initialize some driver and components as following * order: * rti_start --> 0 * BOARD_EXPORT --> 1 * rti_board_end --> 1.end * * DEVICE_EXPORT --> 2 * COMPONENT_EXPORT --> 3 * FS_EXPORT --> 4 * ENV_EXPORT --> 5 * APP_EXPORT --> 6 * * rti_end --> 6.end * * These automatically initializaiton, the driver or component initial function must * be defined with: * INIT_BOARD_EXPORT(fn); * INIT_DEVICE_EXPORT(fn); * ... * INIT_APP_EXPORT(fn); * etc. */ ``` ### 2. 功能实现 ### 根据设计要求应用*RT-Thread Configuration*选择相关组件。 #### 2.1 GPIO的操作(*继电器、按键、LED和蜂鸣器*) #### 打开*RT-Thread Configuration*,使能GPIO驱动,```RT-Thread Components -> Device Drivers -> [*]Using generic GPIO device drivers```,具体应用参考[通用GPIO设备应用笔记](https://www.rt-thread.org/document/site/rtthread-application-note/driver/gpio/an0002-rtthread-driver-gpio/)。 | |pin_name|pin_number(LQFP64)|备注| |:-|:-|:-|:- |继电器1|PC0|8| |继电器2|PC1|9| |继电器3|PC2|10| |继电器4|PC3|11| |LED(绿)|PC7|38| |按键+|PC9|40| |按键-|PC8|39| |按键MODE|PA8|41| |蜂鸣器|PC10|51| GPIO初始化 ``` /* LED indicator initialize */ rt_pin_mode(PIN_MCU_LED, PIN_MODE_OUTPUT); //设置pin为输出模式 /* 4 relay control pin initialize */ rt_pin_mode(PIN_RELAYOUT_SW_1, PIN_MODE_OUTPUT); //设置pin为输出模式 rt_pin_mode(PIN_RELAYOUT_SW_2, PIN_MODE_OUTPUT); //设置pin为输出模式 rt_pin_mode(PIN_RELAYOUT_SW_3, PIN_MODE_OUTPUT); //设置pin为输出模式 rt_pin_mode(PIN_RELAYOUT_SW_4, PIN_MODE_OUTPUT); //设置pin为输出模式 /* button pin initialize */ rt_pin_mode(PIN_BUTTON_PLUS, PIN_MODE_INPUT); rt_pin_mode(PIN_BUTTON_MINUS, PIN_MODE_INPUT); rt_pin_mode(PIN_BUTTON_MODE, PIN_MODE_INPUT); /* BUZZER control pin initialize */ rt_pin_mode(PIN_BUZZER_SW, PIN_MODE_OUTPUT); ``` ##### 2.1.1 按键处理 ##### 新建一个扫描按键的任务,读取按键,若有按键按下,发送相应键值到键值消息队列。新建一个响应按键的任务,读取键值消息队列,并处理相关消息。 采用RT-Thread的消息队列(rt_messagequeue)缓存键值,这里配置消息的内存池key_value_msg_pool[16]为16字节,每条消息为1个字节,按理应该缓存16个消息,经测试只能缓存2条(16÷8)消息,比较奇怪!最开始我定义的内存池为4个字节,结果发送失败,不能正常工作。还需深入了解RT-Thread的消息队列的相关机制。 1. 初始化键值消息队列 ``` /* 消息队列控制块 */ static struct rt_messagequeue mq_key_value; /* 消息队列中用到的放置消息的内存池 */ static rt_uint8_t key_value_msg_pool[16]; /* initialize message queue for key scan */ rt_err_t result; result = rt_mq_init(&mq_key_value, "mq_key", &key_value_msg_pool[0], /* 内存池指向msg_pool */ 1, /* 每个消息的大小是 128 - void* */ sizeof(key_value_msg_pool), /* 内存池的大小是msg_pool的大小 */ RT_IPC_FLAG_FIFO); /* 如果有多个线程等待,按照FIFO的方法分配消息 */ if (RT_EOK != result) { rt_kprintf("init message queue failed.\n"); return -1; } ``` 2. 发送消息 ``` result = rt_mq_send(&mq_key_value, &key_value, sizeof(key_value)); if (RT_EOK != result) { rt_kprintf("rt_mq_send ERR\n"); } ``` 3. 接收消息 ``` if(RT_EOK == rt_mq_recv(&mq_key_value, &key_value, sizeof(key_value), /*RT_WAITING_FOREVER*/10)) { rt_kprintf("recv mq for key 0x%02x\n", key_value); switch(key_value) { case KEY_VALUE_MINUS: // break; case KEY_VALUE_PLUS: break; case KEY_VALUE_MODE: break; default: break; } } ``` ##### 2.1.2 继电器和LED控制 ##### * 继电器控制 ``` if(PIN_HIGH == rt_pin_read(PIN_RELAYOUT_SW_1)) { rt_pin_write(PIN_RELAYOUT_SW_1, PIN_LOW); } else { rt_pin_write(PIN_RELAYOUT_SW_1, PIN_HIGH); } ``` * LED控制 ``` rt_pin_write(PIN_MCU_LED, PIN_LOW); //PIN_HIGH ``` ##### 2.1.3 有源蜂鸣器的控制 ##### 有源蜂鸣器通过gpio控制 #### 2.2 串口的操作(470MHz 无线通讯模块) #### 470MHz无线通讯模块,通过MCU的串口3通讯控制,相关引脚定义: ||pin_name|pin_number|备注 |:-|:-|:-|:- |TXD|PB10|29|MCU发送,无线模块接收 |RXD|PB11|30|MCU接收,无线模块发送 |RST|PB5|57|无线模块复位 |STATUS|PC5|25|无线模块状态 1. 串口初始化 ``` rt_err_t uart_open(const char *name) { rt_err_t res; /* 查找系统中的串口设备 */ uart_device_wireless_module = rt_device_find(name); /* 查找到设备后将其打开 */ if (uart_device_wireless_module != RT_NULL) { res = rt_device_set_rx_indicate(uart_device_wireless_module, uart_intput); if (res != RT_EOK) /* 检查返回值 */ { rt_kprintf("set %s rx indicate error.%d\n",name,res); return -RT_ERROR; } /* 打开设备,以可读写、中断方式 */ res = rt_device_open(uart_device_wireless_module, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_INT_RX ); /* 检查返回值 */ if (res != RT_EOK) { rt_kprintf("open %s device error.%d\n", name, res); return -RT_ERROR; } } else { rt_kprintf("can't find %s device.\n",name); return -RT_ERROR; } /* 初始化事件对象 */ rt_event_init(&uart_470m_event, "uart_470m_event", RT_IPC_FLAG_FIFO); return RT_EOK; } /* 回调函数 */ static rt_err_t uart_intput(rt_device_t dev, rt_size_t size) { /* 发送事件 */ rt_event_send(&uart_470m_event, UART_RX_EVENT); return RT_EOK; } ``` 2. 串口发送 ``` void uart_putchar(const rt_uint8_t c) { rt_size_t len = 0; rt_uint32_t timeout = 0; do { len = rt_device_write(uart_device_wireless_module, 0, &c, 1); timeout++; } while (len != 1 && timeout < 500); } void uart_putstring(rt_uint8_t *pt) { while(*pt) { uart_putchar(*pt++); } } ``` 3. 串口接收和数据帧的处理 ``` /* 串口接收事件标志 */ #define UART_RX_EVENT (1 << 0) #define UART_470M_RECEIVE_BUFFER_SIZE 128 #define UART_470M_MSG_FRAME_TIMEOUT_MS 100 // 100mS /* 事件控制块 */ static struct rt_event uart_470m_event; /* 设备句柄 */ static rt_device_t uart_device_wireless_module = RT_NULL; rt_uint8_t uart_470m_receive_buffer[UART_470M_RECEIVE_BUFFER_SIZE]; rt_uint8_t uart_470m_receive_buffer_pos; rt_uint8_t uart_get_msg_frame(void) { rt_uint32_t e; rt_uint8_t ch; rt_err_t result; uart_470m_receive_buffer_pos = 0; /* 接收事件 */ result = rt_event_recv(&uart_470m_event, UART_RX_EVENT, RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, &e); /* 读取1字节数据 */ while (1 == rt_device_read(uart_device_wireless_module, uart_470m_receive_buffer_pos, uart_470m_receive_buffer + uart_470m_receive_buffer_pos, 1)) { uart_470m_receive_buffer_pos++; if(uart_470m_receive_buffer_pos >= UART_470M_RECEIVE_BUFFER_SIZE) { uart_470m_receive_buffer_pos = 0; } } while(1){ result = rt_event_recv(&uart_470m_event, UART_RX_EVENT, RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR, rt_tick_from_millisecond(UART_470M_MSG_FRAME_TIMEOUT_MS)/*RT_WAITING_FOREVER*/, &e); if(-RT_ETIMEOUT == result) // received a frame of message { uart_470m_receive_buffer[uart_470m_receive_buffer_pos] = 0x00; rt_kprintf("receive message(num_%d):%s\n", uart_470m_receive_buffer_pos, (char*)uart_470m_receive_buffer); uart_putchar('e'); break; } else { /* 读取1字节数据 */ while (1 == rt_device_read(uart_device_wireless_module, uart_470m_receive_buffer_pos, uart_470m_receive_buffer + uart_470m_receive_buffer_pos, 1)) { uart_470m_receive_buffer_pos++; if(uart_470m_receive_buffer_pos >= UART_470M_RECEIVE_BUFFER_SIZE) { uart_470m_receive_buffer_pos = 0; } } } } return ch; } ``` 4. 串口的应用 ``` void uart_470m_thread_entry(void* parameter) { rt_uint8_t uart_rx_data; /* 打开串口 */ if (uart_open("uart3") != RT_EOK) { rt_kprintf("uart open error.\n"); while (1) { rt_thread_delay(10); } } /* 单个字符写 */ uart_putchar('2'); uart_putchar('0'); uart_putchar('1'); uart_putchar('8'); uart_putchar('\n'); /* 写字符串 */ //uart_putstring("Hello RT-Thread!\r\n"); while (1) { /* 读数据 */ //uart_rx_data = uart_getchar(); /* 错位 */ //uart_rx_data = uart_rx_data + 1; /* 输出 */ //uart_putchar(uart_rx_data); uart_get_msg_frame(); } } ``` #### 2.3 SPI flash存储(W25Q128FVSIG) #### 在配置工具中: * RT-Thread Configuration -> RT-Thread Components -> Device Drivers -> 选择Using SPI Bus/Device device drivers和Using W25QXX SPI Norflash; * RT-Thread Configuration -> RT-Thread Components -> Device virtual file system -> 选择 Using device virtual file system和Enable elm-chan fatfs; * RT-Thread Configuration -> Using spi2; SPI flash存储连接的相关引脚定义: ||pin_name|pin_number|备注 |:-|:-|:-|:- |nCS|PB1|27|片选,低电平有效 |SPI2_SCK|PB13|34|SPI2的时钟 |SPI2_MOSI|PB15|36|主出 |SPI2_MISO|PB14|35|主入 由于原bsp对spi flash(W25Qxxx)在工程配置工具menuconfig没有设置好,需修改menuconfig的相关配置文件Kconfig和scons的相关配置文件SConsript。 ① 复制flash存储器的驱动`bsp/stm32fxx-HAL/drv_spiflash.c`至工程,修改`xxx\bsp\stm32f10x-HAL\drivers\SConscript`相关的语句,以便scons工具生成工程。 ``` if GetDepend(['RT_USING_W25QXX']): src += ['drv_spiflash.c'] ``` ② 修改menuconfig的配置文件,添加spi_flash的片选引脚选项和spi的设备名称,xxx\components\drivers\Kconfig ``` config RT_USING_SPI bool "Using SPI Bus/Device device drivers" default n if RT_USING_SPI config RT_USING_W25QXX bool "Using W25QXX SPI NorFlash" default n if RT_USING_W25QXX config RT_W25QXX_CS_PIN int "W25Qxx cs pin" default 27 config RT_W25QXX_SPI_BUS_NAME string "W25Qxx SPI bus name" default "spi2" endif endif ``` **特别注意:由于W25Q128的扇区大小为4096,须设置RT_DFS_ELM_MAX_SECTOR_SIZE为4096,否则不能正常格式化和挂载!!!** ``` #define RT_DFS_ELM_MAX_SECTOR_SIZE 4096 ``` * 格式化flash,挂载 在中断格式化flash的命令为 >mkfs -t elm flash0 挂载的代码如下,验证挂载是否成功可用命令(*df,ls,echo,cat*): ``` rt_device_t device = RT_NULL; char* device_name = "flash0"; // step 1:find device device = rt_device_find(device_name); if( device == RT_NULL) { rt_kprintf(device_name); rt_kprintf("device [%s]: not found!\r\n", device_name); } // step 2:init device if (!(device->flag & RT_DEVICE_FLAG_ACTIVATED)) { rt_err_t result; result = rt_device_init(device); if (result != RT_EOK) { rt_kprintf("To initialize device:%s failed. The error code is %d\r\n", device->parent.name, result); } else { device->flag |= RT_DEVICE_FLAG_ACTIVATED; } } if (dfs_mount(device_name, "/", "elm", 0, 0) == 0) { rt_kprintf("%s mount to / \n", device->parent.name); } else { rt_kprintf("%s mount to / failed!\n", device->parent.name); } ``` #### 2.4 通过无线模块控制继电器 #### 通过470M无线模块控制4路继电器,通讯协议采用Modbus RTU之强制单线圈(功能5指令)。该指令用于强制单个线圈(DO,0X类型)为开或关的状态。 05状态码指令包含线圈地址和设置数据,线圈的起始地址为0000H~0015H(16个线圈的寻址地址);设置数据强制线圈的ON/OFF状态,值FF00H表示线圈处于ON状态,值0000H表示线圈处于OFF状态,其它值对线圈无效。 *** 示例:请求从机17号DO1为开 |从站地址|功能码|DO地址(高位)|DO地址(低位)|值(高位)|值(低位)|CRC16(高位)|CRC16(低位) |-|-|-|-|-|-|-|-|-|-|-|-| |11h|05h|00h|00h|00h|FFh|00h|xxH|xxH| 响应 |从站地址|功能码|DO地址(高位)|DO地址(低位)|值(高位)|值(低位)|CRC16(高位)|CRC16(低位) |-|-|-|-|-|-|-|-|-|-|-|-| |11h|05h|00h|00h|00h|FFh|00h|xxH|xxH| #### 2.5 Modbus RTU从站 #### Modbus RTU协议参考[这里](https://en.wikipedia.org/wiki/Modbus),该Modbus RTU从站占用UART1,如下: |名称|pin_name|pin_num|备注 |-|-|-|- 485_TXD|PA9|42| 485_RXD|PA10|43 RT_EN|PA11|44|发送接收使能开关 1. PORTING 在文件(*portserial.c*)定义使能开关引脚 >#define MODBUS_SLAVE_RT_CONTROL_PIN_INDEX 44 编译提示freemodbus相关文件的assert_param未定义,解决方法是注释掉相关语句即可,这种方法比较粗鲁,以后有时间再整吧。 2. 建立ModbusRTU从站任务 在该项目中的modbusRTU从站相关配置,从站地址0x01,串口号为COM1,波特率为9600,无检验。如下: ``` static void modbus_rtu_slave_thread_entry(void* parameter) { eMBErrorCode err; err = eMBInit(MB_RTU, 0x01, 1, BAUD_RATE_9600, MB_PAR_NONE); if(MB_ENOERR != err){ rt_kprintf("eMBInit fail..."); goto suspend; } eMBEnable(); while (1){ err = eMBPoll(); if(MB_ENOERR != err){ rt_kprintf("eMBPoll states error..."); goto suspend; } else{ rt_kprintf("response..."); } } suspend: rt_thread_suspend(rt_thread_self()); } ``` 3. 测试ModbusRTU从站功能,问题1 用串口助手发送访问从站(0x01)查询指令(0x03功能码), >01 03 00 00 00 01 84 0A 结果有时候响应有时不响应,解决的办法是,延长帧间隔时间,避免接收指令异常,修改文件*mbrtu.c*如下: ``` eMBErrorCode eMBRTUInit( UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate, eMBParity eParity ) { ... usTimerT35_50us = 5 * ( 7UL * 220000UL ) / ( 2UL * ulBaudRate ); ... } ``` 4. 问题2 至此,每次接收到查询指令都能响应,但是回应信息异常,信息的最后两个字节丢失,如应该回应的信息为(*01 03 02 00 00 B8 44*),结果最后的两个字节(*B8 44*)接收不到。 * 问题分析:通过串口发送完回应数据(其实是送至串口发送缓冲区)后,发送接收控制端切换为接收状态,这时串口的数据最后的一两个字节可能还在发送缓冲区,造成回应数据的丢数据。 * 解决方法1:在发送数据以后加延时,文件portserial.c,函数vMBPortSerialEnable(),添加一个延时: ``` void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable) { rt_uint32_t recved_event; rt_thread_delay(rt_tick_from_millisecond(50)); //delay 50mS to avoid lose the last sending bytes if (xRxEnable) ... } ``` * 解决方法2:在串口发送中断函数中,发送完缓冲区内数据后,加一段延时,以便把数据发送完成,再把发送接收使能端切换为接收模式。文件mbrtu.c,函数xMBRTUTransmitFSM(): ``` BOOL xMBRTUTransmitFSM( void ) { BOOL xNeedPoll = FALSE; // assert_param( eRcvState == STATE_RX_IDLE ); switch ( eSndState ) { case STATE_TX_XMIT: /* check if we are finished. */ if( usSndBufferCount != 0 ) { ``` } else { for(int i = 60000; i > 0; i--); // delay to avoid lose the last sending bytes xNeedPoll = xMBPortEventPost( EV_FRAME_SENT ); } break; } return xNeedPoll; } ``` 通过两种方法可以解决发送的最后两个字节被切掉的问题,个人感觉还是方法1稍微靠谱点,方法2添加的延时for语句可能会被有些编译器优化掉。 #### 2.6 TIM之PWM功能验证 #### TIM之PWM功能验证,用于验证设置频率的方法和正确性,相关的宏定义为HAPH_MULTI_CHANNEL_ADC_SAMPLE_CLOCK_FREQ_TEST,相关的文件为*timer_test.c*和*time_test.h*。 >[warning]由于实验的时钟和下节ADC的触发时钟为同一个TIM,故TIM之PWM功能验证和ADC数据采集功能做了互斥处理,即只能同时实现一个功能。 #### 2.7 ADC数据采集 #### ##### 2.7.1 ADC驱动 ##### 采集12个通道的直流信号或50Hz的交流信号的电流值,应用scons工具添加文件**multi_channel_adc.c**和**multi_channel_adc.h**,关联的宏定义: > #define MULTI_CHANNEL_ADC 修改menuconfig工具的配置文件(xxx\bsp\stm32f10x-HAL\Kconfig),添加ADC数据采集项**multi channel adc**: ``` config MULTI_CHANNEL_ADC bool "Using multi channel adc" default n ``` 在目录\bsp\stm32f10x-HAL\applications,添加文件**multi_channel_adc.c**和**multi_channel_adc.h**。 在env工具输入*scons --target=mdk5 -s*,重新生成工程,打开工程,在文件rtconfig.h即可查到新定义的宏*MULTI_CHANNEL_ADC*,并在工程GROUP(Applications)下看到新建的文件*multi_channel_adc.c* 参考的例程为从STM32下载的STM32 HAL官方库的例程,以后有时间可以参考*drv_gpio.c编写ADC相关驱动 > 位于 xxx\STM32Cube_FW_F1_V1.6.0\Projects\STM32F103RB-Nucleo\Examples\ADC\ADC_AnalogWatchdog\MDK-ARM STM32Fxxx ADC通道转换模式的问题: STM32的ADC有单次转换和连续转换2种模式,这两种模式又可以选择是否结合扫描模式。 |CONT=0,SCAN=0| 单次转换模式|单次扫描1通道 |-|-|-| |CONT=1,SCAN=0| 连续转换模式|连续扫描1通道 |CONT=0,SCAN=1| 扫描转换模式| 所有ADC_SQR序列通道转换一次后停止。(单次扫描组) |CONT=1,SCAN=1| 扫描转换模式| 所有ADC_SQR序列通道转换一次后,再从第一个通道循环。连续扫描一组 需要注意的是,如果你的转换序列当中有超过一个通道需要转换的话,那么必须要开启扫描模式,否则的话,始终只转换第一通道。 用ADC1,Regular通道的顺序为Ch0,Ch1,Ch2,Ch3,启动Scan模式 - 在单次转换模式下,启动ADC1,则 1. 开始转换Ch0 1. 转换完成后自动开始转换Ch1 1. 转换完成后自动开始转换Ch2 1. 转换完成后自动开始转换Ch3 1. 转换完成后停止,等待ADC的下一次启动。下一次ADC启动从第一步开始 - 在连续转换模式下,启动ADC1,则 1. 开始转换Ch0 1. 转换完成后自动开始转换Ch1 1. 转换完成后自动开始转换Ch2 1. 转换完成后自动开始转换Ch3 1. 转换完成后回到第一步 如果没启动Sacn模式则上述过程中没有2、3、4这三个步骤 上述前提是Discontinuous模式没有启用。 - 工程配置 1. 配置定时器 这里定时器用作ADC的采样触发时钟,为保证一个周期50Hz交流信号采样20个点,定时器的频率为50×20=1000Hz,周期为1mS。 1. 配置ADC |ADCMODE_SEL1|PA4|20|ADC_CH0模式选择 |-|-|-|-| |ADCMODE_SEL2|PA5|21|ADC_CH1模式选择 |CHSEL0|PB8|61|通道选择 |CHSEL1|PB9|62|通道选择 在文件*stm32f1xx_hal_conf.h*使能HAL库的ADC头文件包含: ``` #define HAL_ADC_MODULE_ENABLED /* 这样才会把ADC相关头文件包含进来如下 */ #ifdef HAL_RCC_MODULE_ENABLED #include "stm32f1xx_hal_rcc.h" #endif /* HAL_RCC_MODULE_ENABLED */ ``` ##### 2.7.2 ADC转换值处理 ##### ADC值在定时器时钟的驱动下采集各通道电压值,通过DMA存入DMA缓冲区,缓冲区满后产生DMA中断,在中断里计算处理各通道电压值。由于采样数据比较多,运算量比较大,中断处理时间过长,导致中断处理还没完,下次中断又来了。 #### 2.8 OLED显示 #### 采用的显示器为OLED_MODULE:QG-2864KSWNG01(1.54inch),控制器为SSD1309,这里使用SPI接口,相关的连线如下: |OLED_RST|PB0|26|复位端| |-|-|-|-| |OLED_CS|PB12|33|片选端 |OLED_SPI_SCK|PB13|34 |OLED_SPI_MOSI|PB15|36 |OLED_D/C|PA12|45|数据/命令选择端 |V_12V_SW|PC6|37|OLED之12V电源开关 *** OLED显示驱动的编写,参考[RT-Thread在Github上托管的示例代码](https://github.com/RT-Thread-packages/samples),SPI接口的OLED显示的示例代码位于目录[samples/driver/spi](https://github.com/RT-Thread-packages/samples/tree/master/driver/spi),在此基础上编写OLED驱动程序,文件名为*drv_ssd1309.c和drv_ssd1309.h* 至此,SPI2总线上挂在了2个设备,一个是flash存储器(w25q128),另一个是OLED显示器,这两个设备均由片选端,同时只能使能一个设备,RT-Thread的SPI驱动保证互斥的使用SPI总线(这是我个人理解,因为我没有刻意的去建一个互斥量来分时使用spi总线,应该是RTT的SPI驱动框架在保证),目前来看这两个设备能正常使用。 #### 2.9 cJSON #### cJSON is C语言编写的超轻量级JSON解析器,可以在menuconfig配置工具中选择使用: >*.config - RT-Thread Configuration -> RT-Thread online packages -> IoT - internet of things -> >[ * ] cJSON: Ultralightweight JSON parser in ANSI C 选择cJSON组件后,下载该组件,重新重新生成工程。 ``` pkgs --update 下载组件 scons --target=mdk5 -s 生成mdk5工程 ``` cJSON组件是开源项目,[托管于github](https://github.com/DaveGamble/cJSON),具体使用参看**README.md** 配置文件的样式: ``` { "slave_address": "CALL", "hw": 4660, "fw": 22136, "ac_range": 20, "dc_range": 100, "wireless": { "pan": 8214, "local": 257, "dst": 1 }, "com1": { "baudrate": 9600, "parity": "odd" } } ``` 1. 创建json - in = cJSON_CreateObject();创建json对象 - cJSON_AddStringToObject();向json对象中添加字符串 - cJSON_AddNumberToObject();向json对象中添加数字 (2)arr = cJSON_CreateArray();创建json数组 cJSON_AddItemToArray(jar,cJSON_CreateNumber(10501));向json数组添加元素 (3)cJSON_AddItemToObject();将一个元组添加到对象中,如把arr添加到in中,可以理解为json中再封装json数据 2. 解析json (1)cJSON_Parse();解析json数据 cJSON_GetObjectItem(json,"data")->valuestring;获取json中data元组的值,字符串 cJSON_GetObjectItem(json,"data")->valueint;获取json中data元组的值,数值 3. 其它 cJSON_ReplaceItemInObject(json,"data",cJSON_CreateString("hello"));用于代替json对象中data元组的值 cJSON_PrintUnformatted(in);对创建的json不带格式输出,即对每一个元组不用换行分隔 cJSON_Delete(in);删除创建的json对象 #### 2.10 W5500驱动 #### STM32F103通过SPI接口连接W5500扩展以太网口,实现了TCP/IP协议,[代码托管于github](https://github.com/thrillerqin/W5500_DEMO)。现在参考[《STM32+W5500+MQTT+Android实现远程数据采集及控制》](https://blog.csdn.net/WIZnet2012/article/details/47401241)实现远程数据采集和控制。 MQTT协议是基于TCP的协议,所以我们只需要在单片机端实现TCP客户端代码之后就很容易移植MQTT了,我在STM32F103RC裸机平台上实现的W5500官方驱动库ioLibrary_Driver,[托管于github](https://github.com/Wiznet/ioLibrary_Driver)。 1. 配置Kconfig文件() 在文件stm32f10x-HAL/Kconfig添加W5500的相关设置项: ``` config RT_USING_W5500 bool "Using W5500 ethernet module" default n if RT_USING_W5500 config RT_W5500_CS_PIN int "W5500 cs pin" default 52 config RT_W5500_SPI_BUS_NAME string "W5500 SPI bus name" default "spi2" endif ``` 生成工程后,在rtconfig.h中自动添加相关宏 ``` #define RT_USING_W5500 #define RT_W5500_CS_PIN 52 #define RT_W5500_SPI_BUS_NAME "spi2" ``` 2. 实现W5500的驱动(drv_w5500.c和drv_w5500.h) W5500以太网模块用的SPI接口,用RT-Thread的SPI驱动框架驱动, |DAI2017|W5500 ethernet module| |-|-| |PB13|SCLK | PB15 |MOSI |PB14 |MISO |PC11 |nCS |PB0 |RST |--|INTn ``` static struct rt_spi_device spi_w5500_dev; /* SPI设备W5500对象 */ static struct stm32_hw_spi_cs spi_w5500_cs; /* SPI设备CS片选引脚 */ /** *@brief W5500复位设置函数 *@param 无 *@return 无 */ void w5500_reset(void) { //与OLED现实模块共用一个复位端,注意只能复位一次,避免两个器件重复复位 // rt_pin_write(W5500_RST_PIN, PIN_LOW); // //wait at least 100ms for reset // rt_thread_delay(100); // rt_pin_write(W5500_RST_PIN, PIN_HIGH); } static int rt_hw_w5500_config(void) { rt_err_t res; /* w5500 CS pin */ spi_w5500_cs.pin = RT_W5500_CS_PIN; rt_pin_mode(spi_w5500_cs.pin, PIN_MODE_OUTPUT); /* 设置片选管脚模式为输出 */ res = rt_spi_bus_attach_device(&spi_w5500_dev, SPI_W5500_DEVICE_NAME, RT_W5500_SPI_BUS_NAME, (void *)&spi_w5500_cs); if (res != RT_EOK) { W5500_TRACE("rt_spi_bus_attach_device(W5500)!\r\n"); return res; } /* config spi */ { struct rt_spi_configuration cfg; cfg.data_width = 8; /* RT_SPI_MODE_0(OK), RT_SPI_MODE_1(NG), RT_SPI_MODE_2(NG), RT_SPI_MODE_3(OK) */ cfg.mode = RT_SPI_MASTER | RT_SPI_MODE_0 | RT_SPI_MSB; cfg.max_hz = 20 * 1000 * 1000; /* 20M,SPI max 42MHz,w5500 4-wire spi */ rt_spi_configure(&spi_w5500_dev, &cfg); } return RT_EOK; } int rt_hw_w5500_init(void) { rt_hw_w5500_config(); // rt_pin_mode(W5500_RST_PIN, PIN_MODE_OUTPUT); // w5500_reset(); //与OLED现实模块共用一个复位端,注意只能复位一次,避免两个器件重复复位 return 0; } //INIT_PREV_EXPORT(rt_hw_w5500_init); /* 使用RT-Thread的组件自动初始化机制 */ /* device initialization */ //INIT_DEVICE_EXPORT(rt_hw_w5500_init);// INIT_EXPORT(fn, "3") /* components initialization (dfs, lwip, ...) */ //INIT_COMPONENT_EXPORT(rt_hw_w5500_init);// INIT_EXPORT(fn, "4") //INIT_APP_EXPORT(rt_hw_w5500_init); /** *@brief 写入一个8位数据到W5500 *@param addrbsb: 写入数据的地址 *@param data:写入的8位数据 *@return 无 */ void IINCHIP_WRITE( uint32_t addrbsb, uint8_t data) { rt_err_t result; uint8_t send_buf1[4]; send_buf1[0] = (addrbsb & 0x00FF0000)>>16; send_buf1[1] = (addrbsb & 0x0000FF00)>> 8; send_buf1[2] = (addrbsb & 0x000000F8) + 4; result = rt_spi_send_then_send(&spi_w5500_dev, send_buf1, 3, &data, 1); if(result != RT_EOK) { W5500_TRACE("w5500 write data error\r\n"); } return; } void WIZCHIP_WRITE(uint32_t AddrSel, uint8_t wb) { IINCHIP_WRITE(AddrSel, wb); } /** *@brief 从W5500读出一个8位数据 *@param addrbsb: 写入数据的地址 *@param data:从写入的地址处读取到的8位数据 *@return 无 */ uint8_t IINCHIP_READ(uint32_t addrbsb) { uint8_t send_buf1[4]; rt_err_t result; uint8_t data; send_buf1[0] = (addrbsb & 0x00FF0000)>>16; send_buf1[1] = (addrbsb & 0x0000FF00)>> 8; send_buf1[2] = (addrbsb & 0x000000F8); result = rt_spi_send_then_recv(&spi_w5500_dev, send_buf1, 3, &data, 1); if(result != RT_EOK) { W5500_TRACE("w5500 read data error\r\n"); } return data; } uint8_t WIZCHIP_READ(uint32_t AddrSel) { IINCHIP_READ(AddrSel); } /** *@brief 向W5500写入len字节数据 *@param addrbsb: 写入数据的地址 *@param buf:写入字符串 *@param len:字符串长度 *@return len:返回字符串长度 */ uint16_t wiz_write_buf(uint32_t addrbsb, uint8_t* buf, uint16_t len) { rt_err_t result; uint8_t send_buf1[4]; send_buf1[0] = (addrbsb & 0x00FF0000)>>16; send_buf1[1] = (addrbsb & 0x0000FF00)>> 8; send_buf1[2] = (addrbsb & 0x000000F8) + 4; result = rt_spi_send_then_send(&spi_w5500_dev, send_buf1, 3, buf, len); if(result == -RT_EIO) { W5500_TRACE("w5500 write buf error\r\n"); return(0); } else { return(len); } } void WIZCHIP_WRITE_BUF(uint32_t AddrSel, uint8_t* pBuf, uint16_t len) { wiz_write_buf(AddrSel, pBuf, len); } /** *@brief 从W5500读出len字节数据 *@param addrbsb: 读取数据的地址 *@param buf:存放读取数据 *@param len:字符串长度 *@return len:返回字符串长度 */ uint16_t wiz_read_buf(uint32_t addrbsb, uint8_t* buf,uint16_t len) { uint8_t send_buf1[4]; rt_err_t result; send_buf1[0] = (addrbsb & 0x00FF0000)>>16; send_buf1[1] = (addrbsb & 0x0000FF00)>> 8; send_buf1[2] = (addrbsb & 0x000000F8); result = rt_spi_send_then_recv(&spi_w5500_dev, send_buf1, 3, buf, len); if(result == -RT_EIO) { W5500_TRACE("w5500 read buf error\r\n"); return(0); } else { return(len); } } void WIZCHIP_READ_BUF (uint32_t AddrSel, uint8_t* pBuf, uint16_t len) { wiz_read_buf(AddrSel, pBuf, len); } ``` 3. W5500初始化 设置W5500的IP、子网掩码、网关和DNS服务器,即缓冲区大小后,即可ping通,之中即可测试应用协议,如MQTT。 #### 2.10 MQTT #### 没有用RT-Thread官方的MQTT组件,他是基于LWIP的,而W5500是硬件实现TCP/IP协议,不能直接用,由于我是刚接触mqtt不会做适应性的改动,所以用[wiznet的官方库里的mqtt例程实现](https://github.com/Wiznet/ioLibrary_Driver)。 目前基本能实现发送和接收mqtt消息。 - 参考[基于STM32F4移植W5500官方驱动库ioLibrary_Driver](https://blog.csdn.net/xiayufeng520/article/details/79602635); - **eclipse paho**](http://www.eclipse.org/paho/)下载[Embedded MQTT](https://github.com/eclipse/paho.mqtt.embedded-c)