## 基于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)