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