메모리 및 데이터 처리
(1) 메모리 MSB와 LSB

- MSB(Most Significant Bit/Byte)는 비트단위에서 최상위 비트를 의미하고, 바이트 단위에서는 최상위 바이트를 의미
- LSB(Least Significant Bit/Byte)는 비트 단위에서 최하위 비트를 의미하고, 바이트 단위에서 최하위 비트를 의미
MSB, LSB 용어는 시리얼 통신 구현 뿐만 아니라 모든 통신에서 LSB 부터 전송할 것인가 MSB부터 전송할 것인가와 같은 규약 (프로토콜)을 정할 때 사용된다.
(2) 메모리 데이터 저장 타입, 리틀 엔디안과 빅 엔디안

- CPU는 데이터를 메모리에 MSB 부터 저장할 것 인지, LSB 부터 저장할 것 인지에 따른 저장 순서에 의해 리틀 엔디안(Little Endian)과 빅 엔디안(Big Endian) 타입으로 분류 된다.
- 그림 처럼 어떤 데이터가 있을 때, 리틀 엔디안 타입은 메모리의 하위 주소에 MSB 부터 저장하는 방식이다.
- 반대로, 빅 엔디안 타입은 메모리의 하위 주소에 LSB 부터 저장하는 방식이다.
- 빅 엔디안 타입은 메모리의 하위 주소 부터 01, 02, 03, 04 데이터를 순서대로 배열되어 있어 사람이 직관적으로 인식하기 편하고 분석에 용이하다.
- 리틀 엔디안 타입은 직관적인 면은 부족하지만 데이터 연산에 강점을 가진다. 예를 들어 2 바이트 크기의 데이터들의 AND 연산 결과에서 LSB 1바이트를 메모리에 저장하고자 한다면 리틀 엔디안은 데이터를 읽어와 AND 연산을 한 결과의 LSB만 바로 저장하면 되지만, 빅 엔디안은 AND 연산을 한 후 시프트 연산을 추가로 한 후 저장해야 한다.
인텔, AMD, ARM, ESP(Xtensa), AVR, STM32 계열의 CPU 들은 리틀 엔디안 방식을 사용한다.
빅 엔디안 방식은 Motorola 68k, PowerPC, MIPS 계열 CPU가 있다.
"네임드" 계열의 CPU는 리틀엔디안을 사용한다.
(3) C 언어 시프트 연산 복습
#include <stdio.h>
// 왼쪽 시프트 <<
int main() {
uint32_t a = 5; // 0000 0101
printf("%u\n", a << 1); // 0000 1010 (10)
printf("%u\n", a << 2); // 0001 0100 (20)
return 0;
}
- a의 비트를 왼쪽으로 n비트 이동하고 오른쪽에는 0을 채움
- 수학적으로 a*(2^n)과 동일
- 데이터형 범위를 초과하는 만큼 시프트하면 정의되지 않은 동작 (UB, Undefined Behavior) 발생 가능
- 예시: 1 << 32 (32비트/4바이트 시스템에서 UB 발생)
#include <stdio.h>
// 오른쪽 시프트 >>
int main() {
unsigned int a = 20; // 0001 0100
printf("%u\n", a >> 1); // 0000 1010 (10)
printf("%u\n", a >> 2); // 0000 0101 (5)
int b = -8; // 1111 1000 (2의 보수 표현)
printf("%d\n", b >> 1); // 결과는 컴파일러마다 다를 수 있음
return 0;
}
- a의 비트를 오른쪽으로 n비트 이동
- 수학적으로 a / (2^n)과 동일
- 부호 비트 유지 여부에 따라 연산 방식이 다름
- 부호 없는 정수 (unsigned int): 0을 채움 (논리 시프트)
- 부호 있는 정수 (signed int):
- 컴파일러에 따라 다름
- 일부 구현에서는 부호 비트를 유지(산술 시프트, 부호 확장)
- 일부 구현에서는 0을 채움(논리 시프트)
- 부호 있는 정수에서 >>는 UB가 될 수도 있음 (컴파일러마다 다르게 동작 가능)
- 부호 있는 정수에서 산술 시프트를 원하면 명확한 캐스팅을 사용하는 것이 좋음
"시프트 연산은 곱셈이나 나눗셈 연산 보다 빠르다."
정수의 곱셈과 나눗셈을 << 또는 >> 로 대체하면 최적화 효과
(4) C 언어 비트 연산 (AND, OR) 복습
#include <stdio.h>
// AND(&) 연산자
int main() {
int a = 5; // 00000101
int b = 3; // 00000011
int result = a & b; // 비트 AND 연산
printf("a & b = %d\n", result); // 00000001 (1 출력)
return 0;
}
- 각 비트가 모두 1일 때만 1, 나머지는 0이 되는 연산
- 특정 비트가 1인지 확인하는 데 사용됨
- 비트 마스킹(Bit Masking)과 플래그 활용에 유용
#include <stdio.h>
// OR (|) 연산자
int main() {
int a = 5; // 00000101
int b = 3; // 00000011
int result = a | b; // 비트 OR 연산
printf("a | b = %d\n", result); // 00000111 (7 출력)
return 0;
}
- 특정 비트를 강제로 1로 설정하는 데 사용됨
" 비트 연산은 빠르지만 논리 연산은 조건 단락 평가가 가능하여 효율적 "
전송과 수신 (TX, RX) 처리
(1) 버퍼

#include <stdio.h>
#include <string.h>
#define BUFFER_SIZE 128
char txBuffer[BUFFER_SIZE]; // 송신 버퍼
int txHead = 0, txTail = 0; // 버퍼의 head/tail 포인터
void sendToBuffer(const char *data) {
int len = strlen(data);
for (int i = 0; i < len; i++) {
txBuffer[txHead] = data[i];
txHead = (txHead + 1) % BUFFER_SIZE; // 순환 버퍼 방식
}
printf("Data added to Tx Buffer: %s\n", data);
}
int main() {
sendToBuffer("Hello, UART!");
return 0;
}
종류 | 설명 | 예제 |
송신 버퍼 (Tx Buffer) | 송신 데이터를 저장하는 버퍼 | UART, 네트워크 데이터 전송 |
수신 버퍼 (Rx Buffer) | 수신 데이터를 임시 저장하는 버퍼 | UART, TCP/IP 패킷 수신 |
순환 버퍼 (Circular Buffer, Ring Buffer) | FIFO(선입선출) 방식의 버퍼로, 오버플로우를 방지 | 실시간 데이터 스트리밍 |
소프트웨어 버퍼 | RAM에서 운영되는 버퍼 | 파일 I/O, 프로그램 내부 데이터 처리 |
하드웨어 버퍼 | MCU, 네트워크 장비에서 내장된 버퍼 | UART, DMA 버퍼 |
- 통신 신호는 하드웨어 물리적 규약에 따라 데이터로 변환된다. (이 부분은 물리적계층에서 처리된다.)
- 버퍼(Buffer)는 통신을 위한 데이터를 임시로 저장하는 메모리 공간
(2) 엔디안 타입에 따른 전송(TX), 수신(RX) 고려사항
#include <stdio.h>
// 송신 측: 16비트 데이터를 리틀 엔디안 순서로 전송
void sendData(uint16_t value) {
unsigned char lowByte = value & 0xFF; // 하위 바이트 (LSB)
unsigned char highByte = (value >> 8) & 0xFF; // 상위 바이트 (MSB)
printf("Sending: 0x%02X 0x%02X\n", lowByte, highByte);
// 송신 버퍼에 넣는다고 가정
unsigned char txBuffer[2] = { lowByte, highByte };
// 수신 측에서 데이터를 받음
receiveData(txBuffer);
}
// 수신 측: 리틀 엔디안 방식으로 데이터를 복원
void receiveData(unsigned char* rxBuffer) {
uint16_t receivedValue = (rxBuffer[1] << 8) | rxBuffer[0];
printf("Received Value: %d (0x%04X)\n", receivedValue, receivedValue);
}
int main() {
uint16_t dataToSend = 16; // 0x0010
sendData(dataToSend);
return 0;
}
- 1바이트 씩 보내는 통신에서 2바이트 정수인 16 (0x0010) 데이터를 전송한다고 가정한다.
- 리틀 엔디안 (하위 메모리부터 LSB 순으로 저장) 전송으로 규약되어 있다면 0x10이 전송되고 0x00이 전송된다.
- 전송되는 순서가 엔디안 규약과 같아야 2바이트의 데이터가 16 이라는 것을 알 수 있다.
- 반대로 전송될 경우 0x1000 (4096)으로 해석되어 엉뚱한 데이터로 인식된다.
- 데이터를 보내는 송신 측과 데이터를 받는 수신 측이 데이터의 전송 순서가 일치되어야 제대로된 해석이 가능하다.
통신 오류 검출
실무에서는 다양한 하드웨어적 노이즈의 원인으로 데이터가 훼손되어 송신 측에서 보낸 데이터와 수신 측에서 받은 데이터가 달라질수 있다. 펌웨어 측면에서 오류를 검출하여 데이터의 무결성을 검사하는 방법이 함께 적용되어야 한다.
(1) 핸드쉐이킹

- 데이터 자체의 오류 검출이라기 보다는 통신이 정상적으로 수행되고 있음을 확인하는 용도
- 송신 측이 데이터를 전송하면 수신 측은 잘 받았다는 신호 (ACK) 또는 못 받았다는 신호 (NACK)를 송신 측에 다시 전달하며 데이터 송신을 진행
- ACK 신호가 없다면 통신 오류로 해석해 볼 수 있다.
- TCP/IP 통신 관점으로 OSI 4계층 TCP에서 수행된다. (3 Way Handshake) ACK를 보내기전 확인을 1번 더한다.
/* 송신 (TX) */
#include <stdio.h>
#include <string.h>
#include "uart.h"
#define UART_PORT 1
void uart_receive() {
char buffer[20];
while (1) {
int len = uart_read(UART_PORT, (uint8_t*)buffer, sizeof(buffer));
if (len > 0) {
buffer[len] = '\0'; // 문자열 종료
// 1. "READY" 수신 → "ACK" 응답
if (strncmp(buffer, "READY", 5) == 0) {
printf("Received READY. Sending ACK...\n");
uart_write(UART_PORT, (uint8_t*)"ACK", 3);
}
// 2. "DATA" 수신 → 처리
else if (strncmp(buffer, "DATA", 4) == 0) {
printf("Data Received: %s\n", buffer);
}
}
}
}
int main() {
uart_init(UART_PORT, 115200);
uart_receive();
return 0;
}
/* 수신 (RX) */
#include <stdio.h>
#include <string.h>
#include "uart.h"
#define UART_PORT 1
void uart_receive() {
char buffer[20];
while (1) {
int len = uart_read(UART_PORT, (uint8_t*)buffer, sizeof(buffer));
if (len > 0) {
buffer[len] = '\0'; // 문자열 종료
// 1. "READY" 수신 → "ACK" 응답
if (strncmp(buffer, "READY", 5) == 0) {
printf("Received READY. Sending ACK...\n");
uart_write(UART_PORT, (uint8_t*)"ACK", 3);
}
// 2. "DATA" 수신 → 처리
else if (strncmp(buffer, "DATA", 4) == 0) {
printf("Data Received: %s\n", buffer);
}
}
}
}
int main() {
uart_init(UART_PORT, 115200);
uart_receive();
return 0;
}
(2) 패리티(Parity) 비트


- 한 개의 통신 바이트에 패러티 비트 1비트를 추가하여 전송하여 데이터의 무결성을 확인
- 데이터 내의 "1" 값의 비트의 수를 짝수(EVEN)로 만드는 방식을 짝수 패리티, 홀수(ODD)로 만드는 방식을 홀수 패리티 방식이라 한다.
- 수신 측에서는 데이터 내의 "1"의 값을 가지는 비트의 수를 세어 정상적인 데이터 인지를 판단한다.
/* 패리티 비트 계산 */
#include <stdio.h>
// 1의 개수를 세는 함수
int count_ones(unsigned char byte) {
int count = 0;
while (byte) {
count += byte & 1; // LSB가 1이면 count 증가
byte >>= 1; // 오른쪽 시프트
}
return count;
}
// 짝수 패리티(EVEN) 비트 추가
unsigned char add_even_parity(unsigned char data) {
int ones = count_ones(data);
unsigned char parity_bit = (ones % 2 == 0) ? 0 : 1; // 짝수 개면 0, 홀수 개면 1
return (data << 1) | parity_bit; // 데이터 왼쪽 이동 후 패리티 비트 추가
}
// 홀수 패리티(ODD) 비트 추가
unsigned char add_odd_parity(unsigned char data) {
int ones = count_ones(data);
unsigned char parity_bit = (ones % 2 == 0) ? 1 : 0; // 짝수 개면 1, 홀수 개면 0
return (data << 1) | parity_bit;
}
(3) 체크썸(Checksum, 검사합) 방식
- 단순 덧셈 방식으로 전송하려는 데이터를 16비트 또는 8비트 단위로 모두 더하여 1의 보수 또는 2의 보수를 취한 값을 데이터와 함께 전송
- 수신 측에서 체크썸 데이터까지 포함하여 받은 데이터를 모두 더하여 데이터의 무결성을 체크
- 2의 보수 방식에서는 체크썸 데이터까지 모든 값을 더한 결과가 0이 나와야 정상 데이터
- 1의 보수 방식에서는 모두 더한 결과가 모든 비트가 1 로 나오면 정상 데이터임을 의미
int main() {
uint16_t data_16bit[] = {0x1234, 0xABCD, 0x5678}; // 예제 데이터 (16비트)
size_t length_16bit = sizeof(data_16bit) / sizeof(data_16bit[0]);
// 16비트 1의 보수 체크섬 계산
uint16_t checksum_16bit = ones_complement_checksum_16bit(data_16bit, length_16bit);
printf("16-bit One's Complement Checksum: 0x%04X\n", checksum_16bit);
// 체크섬 검증
if (verify_ones_complement_16bit(data_16bit, length_16bit, checksum_16bit)) {
printf("Checksum verification successful (One's Complement)\n");
} else {
printf("Checksum verification failed\n");
}
// 16비트 2의 보수 체크섬 계산
uint16_t twos_checksum_16bit = twos_complement_checksum_16bit(data_16bit, length_16bit);
printf("16-bit Two's Complement Checksum: 0x%04X\n", twos_checksum_16bit);
// 2의 보수 검증
if (verify_twos_complement_16bit(data_16bit, length_16bit, twos_checksum_16bit)) {
printf("Checksum verification successful (Two's Complement)\n");
} else {
printf("Checksum verification failed\n");
}
return 0;
}
(4) CRC (Cyclic Redundancy Check)
- 다항식을 이용한 체크썸 방식의 일종으로 단순합 체크썸과 마찬가지로 전송 데이터 후단에 다항식을 이용한 CRC 계산 결과 데이터를 함께 보냄
- 수신 측에서는 CRC 데이터를 체크하여 에러를 검출
- CRC 방식에서는 다항식의 최고차 항의 차수에 따라 CRC-8, CRC-16, CRC-32 등의 방식이 있다.
- 체크썸 방식보다 높은 에러 검출율을 가지고 있어 산업현장에서 많이 사용된다.
- 다음은 현장 프로토콜 중 많이 사용되는 모드버스(MODBUS) 프로토콜의 CRC-16에 대한 사용법이다.
- CRC-16 다항식: 0xA001(표준 MODBUS)
- 이 다항식은 0x8005 (X⁶ + X¹⁵ + 1) 다항식의 반전된 형태
#include <stdio.h>
#include <stdint.h>
// CRC-16 MODBUS 다항식 (0xA001)
#define CRC_POLY 0xA001
// CRC-16 MODBUS 계산 함수
uint16_t crc16_modbus(uint8_t *data, uint16_t length) {
uint16_t crc = 0xFFFF; // 초기값 설정
for (uint16_t i = 0; i < length; i++) {
crc ^= data[i]; // 데이터와 XOR 연산
for (uint8_t j = 0; j < 8; j++) {
if (crc & 1) { // LSB가 1이면 다항식 XOR 적용
crc = (crc >> 1) ^ CRC_POLY;
} else {
crc >>= 1;
}
}
}
return crc; // 16비트 결과 반환
}
// 테스트 데이터 및 CRC 검증
int main() {
uint8_t test_data[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x0A}; // MODBUS 예제 데이터
uint16_t crc = crc16_modbus(test_data, sizeof(test_data));
printf("CRC-16 MODBUS: 0x%04X\n", crc);
// CRC 리틀 엔디안(LSB 우선) 전송 예시
printf("CRC 전송 바이트 순서: 0x%02X 0x%02X\n", crc & 0xFF, (crc >> 8) & 0xFF);
return 0;
}
'임베디드 소프트웨어 > 펌웨어 구현' 카테고리의 다른 글
Stream 클래스 분석 (feat. Serial) (0) | 2025.03.19 |
---|---|
DWIN DGUS HMI 개발 라이브러리 (0) | 2025.03.02 |
FreeRTOS 사용하기 (2) | 2024.10.16 |
실시간 운영체제 (RTOS) (1) | 2024.09.04 |
[GUI & 터치스크린] LVGL 구현 관련 정리 (0) | 2024.07.31 |