RS232標准接口
UART
RS232與UART轉接
下載 NDK 和構建工具
創建支持 C/C++ 的新項目
編譯C/C++代碼
串口通訊原理
關於校驗位
HexString與Bytes的轉換
參考
前言
軟件代碼寫久了,總會對嵌入式開發感興趣,因為軟件的東西寫來寫去看不見摸不着,而嵌入式硬件開發,可以搗鼓一些機械設備玩,電子感應燈,遙控車啥的,這也就是傳說中的創客啊!后面索性買了一套單片機教程《手把手教你學51單片機》和板子學習,作為入門書來說,確實挺不錯,配合上官方的板子,把單片機的基礎原理都將的挺透徹,其中還包括了C語言的講解,為嵌入式開發打下基礎。
共花了2個月把單片機學了一遍,最后覺得也沒有想象中的難,因為其實到頭來還是像在使用工具,只是知識體系會有些不一樣,主要就是基於C/匯編語言通過單片機對寄存器、定時器、每個引腳的高低電平的使用,然后參考硬件手冊,對每個硬件進行操作,以達到想要實現的效果。而如果想要實現自己的最小系統,或者弄出一些好玩的東西,還要自己根據物力電氣原理選原件、畫PCB板子,這就是硬件工程師要干的事情了。
剛好公司項目里基於Android板子開發了一些串口通訊的應用,得益於之前單片機的學習,沒有遇到太多困難,記錄分享一下。
RS232標准接口
也就是PC電腦上所說的COM口,RS232是負邏輯電平,它定義+5~+12V為低電平,而-12~-5V為高電平。
正常情況下,PC台式主機機箱都會有一個RS232的通訊接口(別和VGI的口搞錯啦!),而目前筆記本幾乎不會帶有了,所以都是用USB轉接口。
UART
及Universal Asynchronous Receiver Transmitter:通用異步收發器,通常ARM嵌入式板子都會集成此接口
UART有4個pin(VCC, GND, RX, TX), 用的TTL電平, 低電平為0(0V),高電平為1(3.3V或以上)。
RS232與UART轉接
通常嵌入式里所說的串口,是指UART口,但硬件眾多,大多數都是基於RS232和UART,有時候這兩種口之間需要通訊,最主要不同的其實也就是電平不一樣,所以需要轉接口,某寶上MAX3232種類繁多。
下載 NDK 和構建工具
由於是使用JNI直接進行串口設備的讀寫,所以需要下載工具。android studio版本務必要2.2以上
運行SDK Manager,下載3個工具,CMake、LLDB、NDK
PS:如果下載進度很慢,請使用國內鏡像
創建支持 C/C++ 的新項目
在 Customize C++ Support 選項卡中。你有下面幾種方式來自定義你的項目:
- C++ Standard:點擊下拉框,可以選擇標准 C++,或者選擇默認 CMake 設置的 Toolchain Default 選項。
- Exceptions Support:如果你想使用有關 C++ 異常處理的支持,就勾選它。勾選之后,Android Studio 會在 module 層的 build.gradle 文件中的 cppFlags 中添加 -fexcetions 標志。
- Runtime Type Information Support:如果你想支持 RTTI,那么就勾選它。勾選之后,Android Studio 會在 module 層的 build.gradle 文件中的 cppFlags 中添加 -frtti 標志。
新建成功之后,相對於以前的項目目錄,多了幾個地方。
- cpp 目錄存放你所有 native code 的地方,包括源碼,頭文件,預編譯項目等。對於新項目,Android Studio 創建了一個 C++ 模板文件:native-lib.cpp,並且將該文件放到了你的 app 模塊的 src/main/cpp/ 目錄下。這份模板代碼提供了一個簡答的 C++ 函數:stringFromJNI(),該函數返回一個字符串:”Hello from C++”。
- External Build Files 目錄是存放 CMake 或 ndk-build 構建腳本的地方。有點類似於 build.gradle 文件告訴 Gradle 如何編譯你的 APP 一樣,CMake 和 ndk-build 也需要一個腳本來告知如何編譯你的 native library。對於一個新的項目,Android Studio 創建了一個 CMake 腳本:CMakeLists.txt,並且將其放到了你的 module 的根目錄下。
- gradle的腳本下也加入了externalNativeBuild字段來顯示指定CMake。
PS:如果現有項目需要添加C/C++ 代碼,則參考項目模板添加相應文件即可。
編譯C/C++代碼
CMakeLists.txt
# Sets the minimum version of CMake required to build the native
# library. You should either keep the default value or only pass a
# value of 3.4.0 or lower.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds it for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
serial_port
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
# Associated headers in the same location as their source
# file are automatically included.
src/main/cpp/SerialPort.c )
# Searches for a specified prebuilt library and stores the path as a
# variable. Because system libraries are included in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in the
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
serial_port
# Links the target library to the log library
# included in the NDK.
${log-lib} )
SerialPort.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class android_serialport_api_SerialPort */
#ifndef _Included_android_serialport_api_SerialPort
#define _Included_android_serialport_api_SerialPort
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: android_serialport_api_SerialPort
* Method: open
* Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor;
*/
JNIEXPORT jobject JNICALL Java_android_1serialport_1api_SerialPort_open
(JNIEnv *, jclass, jstring, jint, jint);
/*
* Class: android_serialport_api_SerialPort
* Method: close
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_android_1serialport_1api_SerialPort_close
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
SerialPort.c
/*
* Copyright 2009-2011 Cedric Priscal
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <termios.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <jni.h>
#include "SerialPort.h"
#include "android/log.h"
static const char *TAG="serial_port";
#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##args)
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)
#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)
static speed_t getBaudrate(jint baudrate)
{
switch(baudrate) {
case 0: return B0;
case 50: return B50;
case 75: return B75;
case 110: return B110;
case 134: return B134;
case 150: return B150;
case 200: return B200;
case 300: return B300;
case 600: return B600;
case 1200: return B1200;
case 1800: return B1800;
case 2400: return B2400;
case 4800: return B4800;
case 9600: return B9600;
case 19200: return B19200;
case 38400: return B38400;
case 57600: return B57600;
case 115200: return B115200;
case 230400: return B230400;
case 460800: return B460800;
case 500000: return B500000;
case 576000: return B576000;
case 921600: return B921600;
case 1000000: return B1000000;
case 1152000: return B1152000;
case 1500000: return B1500000;
case 2000000: return B2000000;
case 2500000: return B2500000;
case 3000000: return B3000000;
case 3500000: return B3500000;
case 4000000: return B4000000;
default: return -1;
}
}
/*
* Class: android_serialport_SerialPort
* Method: open
* Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor;
*/
JNIEXPORT jobject JNICALL Java_android_1serialport_1api_SerialPort_open
(JNIEnv *env, jclass thiz, jstring path, jint baudrate, jint flags)
{
int fd;
speed_t speed;
jobject mFileDescriptor;
/* Check arguments */
{
speed = getBaudrate(baudrate);
if (speed == -1) {
/* TODO: throw an exception */
LOGE("Invalid baudrate");
return NULL;
}
}
/* Opening device */
{
jboolean iscopy;
const char *path_utf = (*env)->GetStringUTFChars(env, path, &iscopy);
LOGD("Opening serial port %s with flags 0x%x", path_utf, O_RDWR | flags);
fd = open(path_utf, O_RDWR | flags);
LOGD("open() fd = %d", fd);
(*env)->ReleaseStringUTFChars(env, path, path_utf);
if (fd == -1)
{
/* Throw an exception */
LOGE("Cannot open port");
/* TODO: throw an exception */
return NULL;
}
}
/* Configure device */
{
struct termios cfg;
LOGD("Configuring serial port");
if (tcgetattr(fd, &cfg))
{
LOGE("tcgetattr() failed");
close(fd);
/* TODO: throw an exception */
return NULL;
}
cfmakeraw(&cfg);
cfsetispeed(&cfg, speed);
cfsetospeed(&cfg, speed);
//此處設置校驗位
//cfg.c_cflag……
if (tcsetattr(fd, TCSANOW, &cfg))
{
LOGE("tcsetattr() failed");
close(fd);
/* TODO: throw an exception */
return NULL;
}
}
/* Create a corresponding file descriptor */
{
jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor");
jmethodID iFileDescriptor = (*env)->GetMethodID(env, cFileDescriptor, "<init>", "()V");
jfieldID descriptorID = (*env)->GetFieldID(env, cFileDescriptor, "descriptor", "I");
mFileDescriptor = (*env)->NewObject(env, cFileDescriptor, iFileDescriptor);
(*env)->SetIntField(env, mFileDescriptor, descriptorID, (jint)fd);
}
return mFileDescriptor;
}
/*
* Class: cedric_serial_SerialPort
* Method: close
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_android_1serialport_1api_SerialPort_close
(JNIEnv *env, jobject thiz)
{
jclass SerialPortClass = (*env)->GetObjectClass(env, thiz);
jclass FileDescriptorClass = (*env)->FindClass(env, "java/io/FileDescriptor");
jfieldID mFdID = (*env)->GetFieldID(env, SerialPortClass, "mFd", "Ljava/io/FileDescriptor;");
jfieldID descriptorID = (*env)->GetFieldID(env, FileDescriptorClass, "descriptor", "I");
jobject mFd = (*env)->GetObjectField(env, thiz, mFdID);
jint descriptor = (*env)->GetIntField(env, mFd, descriptorID);
LOGD("close(fd = %d)", descriptor);
close(descriptor);
}
最后java在需要的地方調用
// JNI
private native static FileDescriptor open(String path, int baudrate, int flags);
public native void close();
static {
Log.i(TAG, "loadLibrary..............");
System.loadLibrary("serial_port");
}
串口通訊原理
使用到了linux系統函數,是相對底層的開發,所以一大堆標志位,位運算,看着有些暈,函數的具體使用方法需要查詢手冊
1、打開串口
fd = open(path_utf, O_RDWR | flags);
其中
O_RDWR 讀寫方式打開
O_NOCTTY 不允許進程管理串口
O_NDELAY 非阻塞
2、寫串口
n = write(fd, "ATZ ", 4);
n實際寫入個數
3、設置串口為非阻塞方式
fcntl(fd, F_SETFL, FNDELAY);
4、設置串口為阻塞方式:
fcntl(fd, F_SETFL, 0);
5、讀串口:
res = read(fd,buf,len);
6、關閉串口
Close(fd);
注:示例代碼中通過JAVA的FileDescriptor類包裝了數據流,所以讀寫操作是在上層完成的。
/* Create a corresponding file descriptor */
{
jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor");
jmethodID iFileDescriptor = (*env)->GetMethodID(env, cFileDescriptor, "<init>", "()V");
jfieldID descriptorID = (*env)->GetFieldID(env, cFileDescriptor, "descriptor", "I");
mFileDescriptor = (*env)->NewObject(env, cFileDescriptor, iFileDescriptor);
(*env)->SetIntField(env, mFileDescriptor, descriptorID, (jint)fd);
}
return mFileDescriptor;
關於校驗位
示例代碼默認是沒有添加任何校驗位的,一般情況設備的基礎通訊協議都是
波特率:9600
校驗位:無
數據位:8
停止位:1位
至於具體的指令協議,根據設備提供商文檔自行寫控制代碼即可。
校驗位設置代碼如下:
No parity (8N1):
cfg.c_cflag &= ~PARENB
cfg.c_cflag &= ~CSTOPB
cfg.c_cflag &= ~CSIZE;
cfg.c_cflag |= CS8;
Even parity (7E1):
cfg.c_cflag |= PARENB
cfg.c_cflag &= ~PARODD
cfg.c_cflag &= ~CSTOPB
cfg.c_cflag &= ~CSIZE;
cfg.c_cflag |= CS7;
Odd parity (7O1):
cfg.c_cflag |= PARENB
cfg.c_cflag |= PARODD
cfg.c_cflag &= ~CSTOPB
cfg.c_cflag &= ~CSIZE;
cfg.c_cflag |= CS7;
Space parity is setup the same as no parity (7S1):
cfg.c_cflag &= ~PARENB
cfg.c_cflag &= ~CSTOPB
cfg.c_cflag &= ~CSIZE;
cfg.c_cflag |= CS8;
//cfg.c_cflag |= PARENB | CS8 | CMSPAR;
Mark parity is simulated by using 2 stop bits (7M1):
cfg.c_cflag &= ~PARENB;
cfg.c_cflag |= CSTOPB;
cfg.c_cflag &= ~CSIZE;
cfg.c_cflag |= CS7;
//cfg.c_cflag |= PARENB | CS8 | CMSPAR |PARODD;
1.even 每個字節傳送整個過程中bit為1的個數是偶數個(校驗位調整個數)
2.odd 每個字節穿送整個過程中bit為1的個數是奇數個(校驗位調整個數)
3.noparity沒有校驗位
4.space 校驗位總為0
5.mark 校驗位總為1;
HexString與Bytes的轉換
默認情況下串口讀出來的數據是byte[],有時候需要轉換16進制的字符串進行指令識別
/**
* Created by lixin(178078114@qq.com) on 2016/12/17.
*/
public class HexUtils {
/**
* 把字節數組轉換成16進制字符串
*
* @param bArray
* @return
*/
public static String bytesToHexString(byte[] src) {
StringBuilder stringBuilder = new StringBuilder("");
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString().toUpperCase();
}
/**
* 把16進制字符串轉換成字節數組
*
* @param hex
* @return
*/
public static byte[] hexStringToByte(String hex) {
int len = (hex.length() / 2);
byte[] result = new byte[len];
char[] achar = hex.toCharArray();
for (int i = 0; i < len; i++) {
int pos = i * 2;
result[i] = (byte) (toByte(achar[pos]) << 4 | toByte(achar[pos + 1]));
}
return result;
}
private static byte toByte(char c) {
byte b = (byte) "0123456789ABCDEF".indexOf(c);
return b;
}
}
最終的demo如圖,選擇對了參數和串口設備,即可調試了!
參考
http://gqdy365.iteye.com/blog/2188906
http://wl9739.github.io/2016/09/21/%E5%9C%A8-Android-Studio-2-2-%E4%B8%AD%E6%84%89%E5%BF%AB%E5%9C%B0%E4%BD%BF%E7%94%A8-C-C-md/
demo示例:
鏈接:http://pan.baidu.com/s/1pLc1JVt 密碼:h1zt