// CFMEGA2에서 A0~A3은 0~20mA 전류값을 0~1023의 디지털 값으로 변환한다. (Analog to Digital Converter)
// OMRON E8Y 압력 센서는 압력에 따라 4~20mA의 아날로그 전류를 출력한다.
//
// 4mA의 ADC 값: 약 204 (1023의 20%)
// 20mA의 ADC 값: 1023
//
// 그럼 이론적으로는 CFMEGA2에서 204~1023의 값을 감지할 것이다. (범위: 819)
// 하지만 압력 센서를 연결하고 압력을 가하지 않은 상태의 값을 확인해 보면 195,
// 센서가 감지할 수 있는 최대 압력을 가한 상태의 값을 확인해 보면 985가 나온다. (범위: 790)
//
// 실제 값에 맞게 보정해 주어야 한다.
const int sensorPin = A0; // 0~20mA 전용 입력 핀에 연결 (회색 선).
const float maxPressure = 1000.0f; // 압력 센서 데이터시트에 기재된 최대 측정 압력값. (1,000Pa)
const float maxCurrent = 20.0f; // 압력 센서 데이터시트에 기재된 최대 측정 전류값.
const int initialADCValue = 195; // 압력이 없을때 ADC 값으로 195가 들어온다.
const int maxADCValue = 985; // 최대 압력일때 ADC 값으로 985가 들어온다.
void setup() {
Serial.begin(9600);
Serial.println("----------- CFMEGA2 Pressure Measure Start -----------");
}
void loop() {
int adcValue = analogRead(sensorPin);
// 센서 단선 또는 전원 오류 확인 (4mA 미만 확인)
// 오차를 감안해 ADC 값이 190(약 3.7mA) 미만으로 떨어지면 오류로 간주
if (adcValue < 190) {
Serial.println("Error: Weak Sensor Signal. Check the sensor or the cable.");
} else {
// ADC 값(204~1023)을 압력(0~maxPressure)으로 비례 변환
float pressure = (adcValue - initialADCValue) * (maxPressure / (maxADCValue - initialADCValue));
// 값 보정: 실제 센서에 압력이 전혀 안 걸려 있을 때 출력되는 전류가 정확히
// 4mA(ADC 204)가 아닐 수 있다. 초기값이 미세하게 맞지 않는다면 initialADCValue 값을
// 실제 측정된 초기 ADC 값으로 조금씩 수정하여 보정한다.
// 내 경우엔 195다. 최대 압력이 걸렸을때는 1023이 아니라 985다.
// 센서에서 현재 출력되는 전류(mA) 역산
float current_mA = adcValue * (maxCurrent / maxADCValue);
Serial.print("adc: ");
Serial.print(adcValue);
Serial.print(" | Current: ");
Serial.print(current_mA, 2); // 소수점 둘째 자리까지 출력
Serial.print(" mA | Pressure: ");
Serial.print(pressure, 2);
Serial.println(" Pa");
}
delay(1000); // 1초 대기
}
센서 데이터가 표시된다.
이번엔 아스키 문자가 아닌 바이너리 데이터를 전달해 보자.
const int sensorPin = A0; // 0~20mA 전용 입력 핀에 연결 (회색 선)
const float maxPressure = 1000.0f; // 압력 센서 데이터시트에 기재된 최대 측정 압력값
const float maxCurrent = 20.0f; // 압력 센서 데이터시트에 기재된 최대 측정 전류값
const int initialADCValue = 195; // 압력이 없을때 ADC 값으로 195가 들어온다.
const int maxADCValue = 985; // 최대 압력일때 ADC 값으로 985가 들어온다.
void setup() {
Serial.begin(9600);
//Serial.println("----------- CFMEGA2 Pressure Measure Start -----------");
}
void loop() {
int adcValue = analogRead(sensorPin);
// 센서 단선 또는 전원 오류 확인 (4mA 미만 확인)
// 오차를 감안해 ADC 값이 190(약 3.7mA) 미만으로 떨어지면 오류로 간주
if (adcValue < 190) {
Serial.println("Error: Weak Sensor Signal. Check the sensor or the cable.");
} else {
// ADC 값(204~1023)을 압력(0~maxPressure)으로 비례 변환
float pressure = (adcValue - initialADCValue) * (maxPressure / (maxADCValue - initialADCValue));
// 값 보정: 실제 센서에 압력이 전혀 안 걸려 있을 때 출력되는 전류가 정확히
// 4mA(ADC 204)가 아닐 수 있다. 초기값이 미세하게 맞지 않는다면 initialADCValue 값을
// 실제 측정된 초기 ADC 값으로 조금씩 수정하여 보정한다.
// 내 경우엔 195다. 최대 압력이 걸렸을때는 1023이 아니라 985다.
// 센서에서 현재 출력되는 전류(mA) 역산
float current_mA = adcValue * (maxCurrent / maxADCValue);
//Serial.print("adc: ");
//Serial.print(adcValue);
//Serial.print(" | Current: ");
//Serial.print(current_mA, 2); // 소수점 둘째 자리까지 출력
//Serial.print(" mA | Pressure: ");
//Serial.print(pressure, 2);
//Serial.println(" Pa");
// CFMEGA2(Arduino)의 int는 2바이트(16비트) 크기를 가진다.
Serial.write(highByte((int)pressure)); // 상위 1바이트(8비트) 먼저 전송
Serial.write(lowByte((int)pressure)); // 하위 1바이트(8비트) 이어서 전송
}
delay(1000); // 1초 대기
}
아스키 문자가 아닌 바이너리 데이터이기 때문에 Arduino IDE의 Serial Monitor를 통해 확인하면 알 수 없는 문자가 표시된다.
시리얼 통신용 프로그램을 사용하면 바이너리 데이터를 직접 확인할 수 있다.
1초당 2바이트씩 데이터를 읽는다. 처음 4초간은 압력이 없기 때문에 00 01이 4번 입력되고 5초에 177 Pa의 압력이 가해져 00 B1이 입력되었다.
센서가 출력하는 전류값이나 CFMEGA2가 읽는 전류값은 높은 정밀도를 가질 수 없을 것이다.
압력이 없을 때 센서를 통해 CFMEGA2에 들어온 ADC값이 처음엔 195였는데 몇 번 테스트하는 과정에서 196으로 바뀌었고 변환 과정을 거쳐 압력은 1로 표시되었다. 최대 압력일 때 ADC값도 최초 985에서 986으로 바뀌었다. 986은 변환식을 통해 1001이 된다.
A frame instruction is generally composed of device address, function code, register address, register data, check code, and frame length is related to the function code. Generally, the first byte of each frame data is the device address, which can be set to 1~255. The default is 255(0xFF), and the last two bytes are the CRC check code.
- Turn on relay_1 FF 05 00 00 FF 00 99 E4 3~4 바이트: 릴레이 번호 5~6 바이트: FF00 = 릴레이 켜기, 0000 = 릴레이 끄기 마지막 두 바이트: CRC16 (명령어가 바뀔때마다 다시 계산해야 한다)
- Turn on relay_2 FF 05 00 01 FF 00 C8 24
- Turn off relay_1 FF 05 00 00 00 00 D8 14
- Turn off relay_2 FF 05 00 01 00 00 89 D4
- Turn on all relays FF 0F 00 00 00 08 01 FF 30 1D
- Turn off all relays FF 0F 00 00 00 08 01 00 70 5D
- 3초 후 릴레이 1번 켜기 (켜져 있는 상태에서 하면 꺼졌다가 3초 후 다시 켜진다) FF 10 00 03 00 02 04 00 02 00 1E A5 99 3~4 바이트: 릴레이 번호, 0003=1번, 0008=2번, 000D=3번, 0012=4번, 0017=5번, 001C=6번 10~11 바이트: 딜레이 시간, 10~11바이트 값에 0.1초를 곱하는 숫자가 딜레이 시간, 001E*0.1 = 30*0.1 = 3초
- 5초 후 릴레이 4번 켜기 (켜져 있는 상태에서 하면 꺼졌다가 5초 후 다시 켜진다) FF 10 00 12 00 02 04 00 02 00 32 64 84
- Read device address 00 03 00 00 00 01 85 DB 결과로 돌아오는 값에서 5번째 바이트가 주소 (ex: FF)
- Read baud rate FF 03 03 E8 00 01 11 A4 결과로 돌아오는 값에서 5번째 바이트가 baud rate 0x02=4,800 0x03=9,600 0x04=19,200
아래는 JK-SR-2 시리얼 릴레이 사용법이다.
컴퓨터에 연결할 때 JK-SR-2의 시리얼 포트(RS-232) 9핀을 분리해서 USB to UART 컨버터에 연결해 사용하지 말고 USB to RS-232/DB-9 케이블(컨버터)을 사용하자. RS-232와 UART(TTL)는 신호 레벨이 다르고(12V, 5V) 논리도(정논리, 부논리) 다르기 때문에 UART의 RX, TX, GND 핀을 연결해도 이상하게 작동한다.
실제 연결해서 확인해 보면 논리가 다르기 때문인지, 연결하자마자 아무 메세지를 보내지 않아도 끊임없이 FF값이 수신된다.(위 파란색 제품도 마찬가지일 듯..)
FIS Sensor의 6번(SERIAL) 핀은 Converter의 TXD에 연결해야 한다. RXD에 연결하면 안 된다.
센서에서 데이터가 계속 생성되도록 7번(RST) 핀은 VCC에 연결한다.
import serial
import time
import sys
try:
serialPort = serial.Serial('COM2', 9600, 8, 'N', 1, timeout=1)
# 시리얼 통신 설정. COM3 포트, 9600 보드레이트, 8 데이터 비트, 패리티 없음, 1 스톱 비트, 타임아웃 1초.
time.sleep(1) # 시리얼 연결이 초기화될 때까지 대기
except Exception as e:
print("Serial error: ", e)
sys.exit(0)
try:
while (serialPort.readable()): # 시리얼 포트가 읽을 수 있는 상태인지 확인.
if (serialPort.in_waiting > 0): # 시리얼 버퍼에 대기 중인 데이터가 있는지 확인.
print(serialPort.readline().decode("utf-8", errors="ignore"), end="")
# readline() 메서드를 사용하여 시리얼 포트에서 한 줄씩 데이터를 읽고 UTF-8로 디코딩하여 출력.
# 오류가 발생할 경우 무시하도록 설정.
else:
print("No data waiting in the serial buffer.")
time.sleep(0.2)
except KeyboardInterrupt:
print("\n[알림] Ctrl+C 입력 감지. 프로그램을 종료합니다.")
finally:
if 'serialPort' in locals() and serialPort.is_open:
# locals() 함수를 사용하여 serialPort 변수가 정의되어 있는지 확인하고, 시리얼 포트가 열려 있는지 확인.
serialPort.close()
print("시리얼 포트가 안전하게 닫혔습니다.")
#include <windows.h>
#include <iostream>
#include <string>
int main() {
std::cout << "Available COM Ports: \n";
for (int i = 1; i < 256; ++i) {
std::string portName = "\\\\.\\COM" + std::to_string(i);
// \\.\com (코드 작성 시 이스케이프 문자를 적용한 \\\\.\\com)는 Windows 운영체제에서 직렬 포트(Serial Port)나 병렬 포트(LPT) 같은 하드웨어 장치에 직접 접근하기 위한 경로 형식이다.
// 이 경로는 일반적으로 COM 포트에 접근할 때 사용된다. 예를 들어, COM1 포트에 접근하려면 "\\\\.\\COM1"과 같은 형식으로 경로를 지정한다.
// 이 방식은 Windows에서 하드웨어 장치와 통신하기 위해 사용되는 표준적인 방법 중 하나이다.
// 포트 번호가 10 이하인 경우에는 "COM1", "COM2"와 같이 간단히 사용할 수 있지만, 포트 번호가 10 이상인 경우에는 "\\\\.\\COM10"과 같이 전체 경로를 사용해야 한다.
HANDLE hComm = CreateFileA(
portName.c_str(), // 만들거나 열 파일 또는 디바이스의 이름.
GENERIC_READ | GENERIC_WRITE, //파일 또는 디바이스에 대한 요청된 액세스이며 읽기, 쓰기, 둘 다 또는 0으로 요약할 수 있다.
NULL, // 파일 또는 디바이스의 요청된 공유 모드.
NULL, // 보안 속성에 대한 포인터.
OPEN_EXISTING, // 파일이 존재하는 경우에만 열고, 그렇지 않으면 실패.
NULL, // 파일 또는 디바이스에 대한 플래그 및 속성.
NULL); // 템플릿 파일 핸들 또는 디바이스 핸들로 사용할 수 있는 유효한 핸들. 이 매개변수는 CreateFile이 새 파일을 만들 때만 사용. 이 매개변수는 일반적으로 NULL로 설정.
if (hComm != INVALID_HANDLE_VALUE) {
std::cout << "-> COM" << i << " is available.\n";
CloseHandle(hComm);
}
}
return 0;
}
C, QueryDosDevice 예제
#include <windows.h>
#include <stdio.h>
int main() {
char deviceName[256];
char comPort[16];
char openPortName[32];
printf("--- 사용 가능한 COM 포트 목록 ---\n");
// COM1부터 COM256까지 가능한 포트 번호를 순회하며 확인
for (int i = 1; i < 256; i++) {
sprintf_s(comPort, sizeof(comPort), "COM%d", i);
// QueryDosDevice를 이용해 해당 포트가 존재하는지 확인
DWORD result = QueryDosDeviceA(comPort, deviceName, sizeof(deviceName));
// CreateFile 함수로 포트를 열 때는 \\\\.\\COM10 형식을 써야 하지만, QueryDosDevice로
// 시스템에 등록된 장치 이름을 조회할 때는 접두사(\\.\)를 붙이면 안된다.
// 이 함수는 순수한 커널 장치 이름(예: COM1, COM10)만 인자로 받도록 설계되어 있다.
if (result != 0) { // result는 deviceName에 복사된 문자열의 길이.
// 실제 포트를 열거나 사용할 때는 "\\\\.\\" 접두사를 붙여서 출력 및 활용한다.
sprintf_s(openPortName, sizeof(openPortName), "\\\\.\\%s", comPort);
printf("%s 연결됨 (장치명: %s)\n", openPortName, deviceName);
}
}
return 0;
}
어떤 프로그램이 시리얼 포트를 사용 중인지 조사할 때 File Handle의 이름으로 위 그림의 해당 장치명을 지정해야 찾을 수 있다.
Process Explorer - Find - Find Handle or DLL... 클릭 - Handle or DLL substring에 장치명을 입력하고 Search 클릭
시리얼 포트가 사용중이라면 사용하고 있는 프로세스가 표시된다. python.exe가 사용 중이다.
장치관리자에서는 포트 - 속성 - 자세히 - 서비스 - 값에 표시된 이름을 이용하자.
C# 예제
using System;
using System.IO.Ports;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
// PC에서 사용 가능한 모든 시리얼 포트 배열 가져오기
string[] ports = SerialPort.GetPortNames();
Console.WriteLine("사용 가능한 시리얼 포트:");
foreach (string port in ports)
{
Console.WriteLine(port);
}
// GetPortNames()는 PC에 잡혀있는 모든 포트를 반환하므로, 특정 포트가 현재 다른 프로그램에서
// 사용 중이거나 연결 가능한 상태인지 확인하려면 직접 Open()을 시도해 봐야 한다.
Console.WriteLine(Environment.NewLine + "연결 가능한 시리얼 포트:");
foreach (string port in ports)
{
using (SerialPort serialPort = new SerialPort(port))
{
try
{
serialPort.Open();
Console.WriteLine($"{port} : 연결 가능 (사용 가능)");
serialPort.Close();
}
catch (UnauthorizedAccessException)
{
Console.WriteLine($"{port} : 접근 거부 (다른 프로그램에서 사용 중)");
}
catch (Exception ex)
{
Console.WriteLine($"{port} : 오류 발생 - {ex.Message}");
}
}
}
}
}
}
다른 프로그램에서 사용중인 경우
Python, pyserial 예제
import serial.tools.list_ports
# 사용 가능한 포트 리스트 가져오기
ports = serial.tools.list_ports.comports()
print("연결된 시리얼 포트 목록:")
print("-" * 30)
if ports:
for port in ports:
print(f"포트 이름: {port.device}")
print(f"설명: {port.description}")
print(f"하드웨어 ID: {port.hwid}")
print("-" * 30)
else:
print("사용 가능한 시리얼 포트가 없습니다.")
"""
# 각 포트에 대해 연결 가능 여부 확인
import serial
try:
for port in ports:
ser = serial.Serial(port.device)
if ser.is_open:
print(f"{port.device} : 연결 가능 (사용 가능)")
ser.close()
else:
print(f"{port.device} : 연결 불가능 (사용 중)")
except serial.SerialException as e:
print(f"시리얼 포트 확인 중 오류 발생: {e}")
"""
import serial
import time
import sys
try:
serialPort = serial.Serial('COM4', 9600, 8, 'N', 1, timeout=1)
# 시리얼 통신 설정. COM4 포트, 9600 보드레이트, 8 데이터 비트, 패리티 없음, 1 스톱 비트, 타임아웃 1초.
time.sleep(2) # 시리얼 연결이 초기화될 때까지 대기
except Exception as e:
print("Serial error: ", e)
sys.exit(0)
while (serialPort.readable()): # 시리얼 포트가 읽을 수 있는 상태인지 확인.
if (serialPort.in_waiting > 0): # 시리얼 버퍼에 대기 중인 데이터가 있는지 확인.
print(serialPort.readline().decode("utf-8", errors="ignore"), end="")
# readline() 메서드를 사용하여 시리얼 포트에서 한 줄씩 데이터를 읽고 UTF-8로 디코딩하여 출력.
# 오류가 발생할 경우 무시하도록 설정.
time.sleep(0.1)
serialPort.close()