[STM32#27] stm32를 modbus rtu 슬레이브(slave)로 C# winform을 마스터(master)로 설정해서 제어하는 방법(3)!(녹칸다 내맘대로 STM32)
프로그래밍/STM32 2026. 1. 19. 23:05
https://youtube.com/live/5ZRg9t2ga8o
[STM32#27] stm32를 modbus rtu 슬레이브(slave)로 C# winform을 마스터(master)로 설정해서 제어하는 방법(3)!(녹칸다 내맘대로 STM32)
심심한녹칸다의 내맘대로 STM32시리즈이다!
STM32시리즈의 모든 자료는 구글 슬라이드에 작성하고 모두에게 공유되어있음!
https://docs.google.com/presentation/d/1myA5iYbjuKsLWLqtRLKAiRfwUwvqB1d1RGjiMIIgp3I/edit?slide=id.g3b7a28c0ef6_1_0#slide=id.g3b7a28c0ef6_1_0
read holding register부터 이어서 해보기!
1.녹칸다의 STM32 26편-6번예제에서 C#으로 만든 modbus rtu마스터가 1개의 word를 read holding register하도록 해놓았는데, 2개이상도 가능하도록 수정하시오!

/* USER CODE BEGIN 0 */
uint16_t cal_crc(uint8_t *data, uint8_t len)
{
uint16_t crc = 0xFFFF;
for(uint8_t i = 0; i < len; i++)
{
crc ^= data[i]; // LSB부터 XOR
for(uint8_t j = 0; j < 8; j++)
{
if(crc & 0x0001)
{
crc >>= 1;
crc ^= 0xA001; // Modbus polynomial
}
else
{
crc >>= 1;
}
}
}
return crc;
}
/* USER CODE END 0 */
/* USER CODE BEGIN 2 */
uint8_t slave_id = 0x01;
uint8_t tx_buff[100];
uint8_t req[8];
uint8_t res[50];
uint16_t w_reg[6] = {0}; //6word 정도의 레지스터가 있다고 가정함
/* USER CODE END 2 */
while (1)
{
//usart1은 RS485통신, usart2는 PC와 통신
//usart1으로 수신해서 usart2로 결과를 송신한다!
if(HAL_UART_Receive(&huart1, req, 8, 500) == HAL_OK){
//응답예시데이터
uint8_t id = req[0];
//내 id에게 req가 왔을때만 res를 전송한다!
if(id == slave_id){
uint8_t fc = req[1];
if(fc == 0x06){
uint16_t reg_addr = (req[2] << 8) | req[3];
uint16_t reg_data = (req[4] << 8) | req[5];
uint16_t crc16 = (req[7] << 8) | req[6];
//STM32가 CRC16을 계산함!
uint16_t my_crc16 = cal_crc(req,6);
if(crc16 == my_crc16){
sprintf(tx_buff,"id=%02X, fc=%02X, addr=%04X, data=%04X, crc=%04X\n",id,fc,reg_addr,reg_data,crc16);
HAL_UART_Transmit(&huart2, tx_buff, strlen(tx_buff), 100);
w_reg[reg_addr] = reg_data;
//레지스터가 6개 있으니까 6개의 값을 출력한다!
sprintf(tx_buff,"w_reg=%d, %d, %d, %d, %d, %d\n",w_reg[0],w_reg[1],w_reg[2],w_reg[3],w_reg[4],w_reg[5]);
HAL_UART_Transmit(&huart2, tx_buff, strlen(tx_buff), 100);
//응답은 req를 그대로 응답하면 된다!
HAL_UART_Transmit(&huart1, req, 8, 100);
}else{
HAL_UART_Transmit(&huart2, "CRC16 ERROR!\n", 13, 100);
}
}else if(fc == 0x03){
//read holding register
uint16_t start_addr = (req[2] << 8) | req[3];
uint16_t count = (req[4] << 8) | req[5];
uint16_t crc16 = (req[7] << 8) | req[6];
//STM32가 CRC16을 계산함!
uint16_t my_crc16 = cal_crc(req,6);
if(crc16 == my_crc16){
sprintf(tx_buff,"id=%02X, fc=%02X, addr=%04X, data=%04X, crc=%04X\n",id,fc,start_addr,count,crc16);
HAL_UART_Transmit(&huart2, tx_buff, strlen(tx_buff), 100);
//마스터가 요구한 워드에 갯수만큼 응답한다!
//0~2번까지는 자리가 고정이다!
res[0] = id;
res[1] = fc;
res[2] = count * 2; //count는 무조건 1인상태!
//count == 2
//반복문
for(int i = 0;i < count * 2;i++){
//i의 값을 2로 나눠서 나머지가 없으면 짝수다!
if(i%2 == 0){
//짝수
res[3+i] = w_reg[start_addr+(i/2)] >> 8; //상위8bit
}else{
//홀수
res[3+i] = w_reg[start_addr+(i/2)] & 0xFF; //하위8bit
}
}
//count가 2이라면~ [0,1,2,3,4,5,6] [7,8]
//CRC16의 위치는 3+count*2, 4+count*2
//CRC16이전의 데이터로 체크섬을 계산한다!
uint16_t res_crc16 = cal_crc(res,3+count*2);
res[3+count*2] = (res_crc16 & 0xFF);
res[4+count*2] = (res_crc16 >> 8);
//3+count*2에다가 체크섬 갯수 2개를 더한개 최종보낼 데이터의 길이
HAL_UART_Transmit(&huart1, res, 5+count*2, 100);
HAL_UART_Transmit(&huart2, "RES DATA = ", 11, 100);
for(int i = 0;i<5+count*2;i++){
sprintf(tx_buff,"%02X, ",res[i]);
HAL_UART_Transmit(&huart2, tx_buff, strlen(tx_buff), 100);
}
HAL_UART_Transmit(&huart2, "\n", 1, 100);
}else{
HAL_UART_Transmit(&huart2, "CRC16 ERROR!\n", 13, 100);
}
}
}
}
using Modbus.Device; //네임스페이스를 추가!
//nmodbus4 객체선언(modbus rtu전용)
ModbusSerialMaster msm;
private void button1_Click(object sender, EventArgs e)
{
//접속버튼 클릭
serialPort1.BaudRate = 9600;
serialPort1.PortName = textBox1.Text;
serialPort1.Open();
if (serialPort1.IsOpen)
{
//접속이 완료된 serialport 객체를 라이브러리에게 넘긴다!
msm = ModbusSerialMaster.CreateRtu(serialPort1);
MessageBox.Show("연결완료!");
}
}
private void button2_Click(object sender, EventArgs e)
{
ushort addr = ushort.Parse(textBox2.Text);
ushort value = ushort.Parse(textBox3.Text);
msm.WriteSingleRegister(1, addr, value);
}
private void button3_Click(object sender, EventArgs e)
{
ushort addr = ushort.Parse(textBox4.Text);
ushort cnt = ushort.Parse(textBox5.Text);
ushort[] data = msm.ReadHoldingRegisters(1, addr, cnt);
richTextBox1.Text += "응답데이터=\n";
for(int i = 0; i < data.Length; i++)
{
richTextBox1.Text += data[i] + "\n";
}
}
2.(지금부터 새로운예제로 구현하기) read holding register를 참고해서 read input register를 구현한다음 stm32의 내부 메모리값을 읽어오시오!

/* USER CODE BEGIN 0 */
uint16_t cal_crc(uint8_t *data, uint8_t len)
{
uint16_t crc = 0xFFFF;
for(uint8_t i = 0; i < len; i++)
{
crc ^= data[i]; // LSB부터 XOR
for(uint8_t j = 0; j < 8; j++)
{
if(crc & 0x0001)
{
crc >>= 1;
crc ^= 0xA001; // Modbus polynomial
}
else
{
crc >>= 1;
}
}
}
return crc;
}
/* USER CODE END 0 */
/* USER CODE BEGIN 2 */
uint8_t slave_id = 0x01;
uint8_t tx_buff[100];
uint8_t req[8];
uint8_t res[50];
//
uint16_t r_reg[6] = {111,222,333,444,555,666}; //dummy data
/* USER CODE END 2 */
while (1)
{
//usart1은 RS485통신, usart2는 PC와 통신
//usart1으로 수신해서 usart2로 결과를 송신한다!
if(HAL_UART_Receive(&huart1, req, 8, 500) == HAL_OK){
//응답예시데이터
uint8_t id = req[0];
//내 id에게 req가 왔을때만 res를 전송한다!
if(id == slave_id){
uint8_t fc = req[1];
if(fc == 0x04){
//read input register
uint16_t start_addr = (req[2] << 8) | req[3];
uint16_t count = (req[4] << 8) | req[5];
uint16_t crc16 = (req[7] << 8) | req[6];
//STM32가 CRC16을 계산함!
uint16_t my_crc16 = cal_crc(req,6);
if(crc16 == my_crc16){
sprintf(tx_buff,"id=%02X, fc=%02X, addr=%04X, data=%04X, crc=%04X\n",id,fc,start_addr,count,crc16);
HAL_UART_Transmit(&huart2, tx_buff, strlen(tx_buff), 100);
//마스터가 요구한 워드에 갯수만큼 응답한다!
//0~2번까지는 자리가 고정이다!
res[0] = id;
res[1] = fc;
res[2] = count * 2; //count는 무조건 1인상태!
//count == 2
//반복문
for(int i = 0;i < count * 2;i++){
//i의 값을 2로 나눠서 나머지가 없으면 짝수다!
if(i%2 == 0){
//짝수
res[3+i] = r_reg[start_addr+(i/2)] >> 8; //상위8bit
}else{
//홀수
res[3+i] = r_reg[start_addr+(i/2)] & 0xFF; //하위8bit
}
}
//count가 2이라면~ [0,1,2,3,4,5,6] [7,8]
//CRC16의 위치는 3+count*2, 4+count*2
//CRC16이전의 데이터로 체크섬을 계산한다!
uint16_t res_crc16 = cal_crc(res,3+count*2);
res[3+count*2] = (res_crc16 & 0xFF);
res[4+count*2] = (res_crc16 >> 8);
//3+count*2에다가 체크섬 갯수 2개를 더한개 최종보낼 데이터의 길이
HAL_UART_Transmit(&huart1, res, 5+count*2, 100);
HAL_UART_Transmit(&huart2, "RES DATA = ", 11, 100);
for(int i = 0;i<5+count*2;i++){
sprintf(tx_buff,"%02X, ",res[i]);
HAL_UART_Transmit(&huart2, tx_buff, strlen(tx_buff), 100);
}
HAL_UART_Transmit(&huart2, "\n", 1, 100);
}else{
HAL_UART_Transmit(&huart2, "CRC16 ERROR!\n", 13, 100);
}
}
}
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
using Modbus.Device;
ModbusSerialMaster msm;
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
//접속버튼 클릭
serialPort1.PortName = textBox1.Text;
serialPort1.BaudRate = 9600;
serialPort1.Open();
if (serialPort1.IsOpen)
{
msm = ModbusSerialMaster.CreateRtu(serialPort1);
MessageBox.Show("접속완료");
}
else
{
MessageBox.Show("접속실패");
}
}
private void button2_Click(object sender, EventArgs e)
{
ushort addr = ushort.Parse(textBox2.Text);
ushort cnt = ushort.Parse(textBox3.Text);
ushort[] data = msm.ReadInputRegisters(1, addr, cnt);
richTextBox1.Text = "수신데이터=\n";
for(int i = 0; i < data.Length; i++)
{
richTextBox1.Text += data[i] + "\n";
}
}
3.예제2에서 만든 수신구문은 마스터가 보낸 request를 8개가 다 올때까지 기다리는 형식이라서 blocking이 너무 심하게 걸린다! RTOS를 적용하고 PB1에 연결된 버튼을 누르면 1씩 다운카운트되고, PA8에 연결된 버튼을 누르면 1씩 업카운트될때, 그 카운터값이 읽기전용 레지스터 0번지에 지정되었다고 할때 마스터에서 그 값을 읽을 수 있도록 하시오!
-task1 : 마스터에서 modbus rtu명령이 오면 응답한다
-task2 : 버튼을 누르면 업/다운 카운트한다

/* USER CODE BEGIN 0 */
uint16_t cal_crc(uint8_t *data, uint8_t len)
{
uint16_t crc = 0xFFFF;
for(uint8_t i = 0; i < len; i++)
{
crc ^= data[i]; // LSB부터 XOR
for(uint8_t j = 0; j < 8; j++)
{
if(crc & 0x0001)
{
crc >>= 1;
crc ^= 0xA001; // Modbus polynomial
}
else
{
crc >>= 1;
}
}
}
return crc;
}
//task1과 task2가 동시에 참조하는 메모리
volatile uint16_t r_reg[6] = {0};
/* USER CODE END 0 */
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN 5 */
uint8_t slave_id = 0x01;
uint8_t tx_buff[100];
uint8_t req[50];
uint8_t res[50];
/* Infinite loop */
for(;;)
{
if(HAL_UART_Receive(&huart1, req, 8, 500) == HAL_OK){
//응답예시데이터
uint8_t id = req[0];
//내 id에게 req가 왔을때만 res를 전송한다!
if(id == slave_id){
uint8_t fc = req[1];
if(fc == 0x04){
//read input register
uint16_t start_addr = (req[2] << 8) | req[3];
uint16_t count = (req[4] << 8) | req[5];
uint16_t crc16 = (req[7] << 8) | req[6];
//STM32가 CRC16을 계산함!
uint16_t my_crc16 = cal_crc(req,6);
if(crc16 == my_crc16){
sprintf(tx_buff,"id=%02X, fc=%02X, addr=%04X, data=%04X, crc=%04X\n",id,fc,start_addr,count,crc16);
HAL_UART_Transmit(&huart2, tx_buff, strlen(tx_buff), 100);
//마스터가 요구한 워드에 갯수만큼 응답한다!
//0~2번까지는 자리가 고정이다!
res[0] = id;
res[1] = fc;
res[2] = count * 2; //count는 무조건 1인상태!
//count == 2
//반복문
for(int i = 0;i < count * 2;i++){
//i의 값을 2로 나눠서 나머지가 없으면 짝수다!
if(i%2 == 0){
//짝수
res[3+i] = r_reg[start_addr+(i/2)] >> 8; //상위8bit
}else{
//홀수
res[3+i] = r_reg[start_addr+(i/2)] & 0xFF; //하위8bit
}
}
//count가 2이라면~ [0,1,2,3,4,5,6] [7,8]
//CRC16의 위치는 3+count*2, 4+count*2
//CRC16이전의 데이터로 체크섬을 계산한다!
uint16_t res_crc16 = cal_crc(res,3+count*2);
res[3+count*2] = (res_crc16 & 0xFF);
res[4+count*2] = (res_crc16 >> 8);
//3+count*2에다가 체크섬 갯수 2개를 더한개 최종보낼 데이터의 길이
HAL_UART_Transmit(&huart1, res, 5+count*2, 100);
HAL_UART_Transmit(&huart2, "RES DATA = ", 11, 100);
for(int i = 0;i<5+count*2;i++){
sprintf(tx_buff,"%02X, ",res[i]);
HAL_UART_Transmit(&huart2, tx_buff, strlen(tx_buff), 100);
}
HAL_UART_Transmit(&huart2, "\n", 1, 100);
}else{
HAL_UART_Transmit(&huart2, "CRC16 ERROR!\n", 13, 100);
}
}
}
}
osDelay(1);
}
/* USER CODE END 5 */
}
void StartTask02(void *argument)
{
/* USER CODE BEGIN StartTask02 */
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == 0){
r_reg[0]--;
osDelay(200);
}
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == 0){
r_reg[0]++;
osDelay(200);
}
osDelay(1); //반드시 있어야함
}
/* USER CODE END StartTask02 */
}
ModbusSerialMaster msm;
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
//접속버튼 클릭
serialPort1.PortName = textBox1.Text;
serialPort1.BaudRate = 9600;
serialPort1.Open();
if (serialPort1.IsOpen)
{
msm = ModbusSerialMaster.CreateRtu(serialPort1);
MessageBox.Show("접속완료");
}
else
{
MessageBox.Show("접속실패");
}
}
private void button2_Click(object sender, EventArgs e)
{
timer1.Start();
}
private void timer1_Tick(object sender, EventArgs e)
{
//타이머인터럽트가 0.1초마다 뭐하면되는냐?
ushort[] data = msm.ReadInputRegisters(1, 0, 1);
textBox4.Text = ((short)data[0]).ToString();
}
4.녹칸다의 STM32쉴드에 붙어있는 가변저항의 값을 읽기전용 레지스터 1번지에 저장을해서 마스터 화면에 출력하시오!
-task1 : 마스터에서 modbus rtu명령이 오면 응답한다
-task2 : 버튼을 누르면 업/다운 카운트한다
-task3 : ADC값 측정하기~

/* USER CODE BEGIN 0 */
uint16_t cal_crc(uint8_t *data, uint8_t len)
{
uint16_t crc = 0xFFFF;
for(uint8_t i = 0; i < len; i++)
{
crc ^= data[i]; // LSB부터 XOR
for(uint8_t j = 0; j < 8; j++)
{
if(crc & 0x0001)
{
crc >>= 1;
crc ^= 0xA001; // Modbus polynomial
}
else
{
crc >>= 1;
}
}
}
return crc;
}
//task1과 task2가 동시에 참조하는 메모리
volatile uint16_t r_reg[6] = {0};
/* USER CODE END 0 */
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN 5 */
uint8_t slave_id = 0x01;
uint8_t tx_buff[100];
uint8_t req[50];
uint8_t res[50];
/* Infinite loop */
for(;;)
{
if(HAL_UART_Receive(&huart1, req, 8, 500) == HAL_OK){
//응답예시데이터
uint8_t id = req[0];
//내 id에게 req가 왔을때만 res를 전송한다!
if(id == slave_id){
uint8_t fc = req[1];
if(fc == 0x04){
//read input register
uint16_t start_addr = (req[2] << 8) | req[3];
uint16_t count = (req[4] << 8) | req[5];
uint16_t crc16 = (req[7] << 8) | req[6];
//STM32가 CRC16을 계산함!
uint16_t my_crc16 = cal_crc(req,6);
if(crc16 == my_crc16){
sprintf(tx_buff,"id=%02X, fc=%02X, addr=%04X, data=%04X, crc=%04X\n",id,fc,start_addr,count,crc16);
HAL_UART_Transmit(&huart2, tx_buff, strlen(tx_buff), 100);
//마스터가 요구한 워드에 갯수만큼 응답한다!
//0~2번까지는 자리가 고정이다!
res[0] = id;
res[1] = fc;
res[2] = count * 2; //count는 무조건 1인상태!
//count == 2
//반복문
for(int i = 0;i < count * 2;i++){
//i의 값을 2로 나눠서 나머지가 없으면 짝수다!
if(i%2 == 0){
//짝수
res[3+i] = r_reg[start_addr+(i/2)] >> 8; //상위8bit
}else{
//홀수
res[3+i] = r_reg[start_addr+(i/2)] & 0xFF; //하위8bit
}
}
//count가 2이라면~ [0,1,2,3,4,5,6] [7,8]
//CRC16의 위치는 3+count*2, 4+count*2
//CRC16이전의 데이터로 체크섬을 계산한다!
uint16_t res_crc16 = cal_crc(res,3+count*2);
res[3+count*2] = (res_crc16 & 0xFF);
res[4+count*2] = (res_crc16 >> 8);
//3+count*2에다가 체크섬 갯수 2개를 더한개 최종보낼 데이터의 길이
HAL_UART_Transmit(&huart1, res, 5+count*2, 100);
HAL_UART_Transmit(&huart2, "RES DATA = ", 11, 100);
for(int i = 0;i<5+count*2;i++){
sprintf(tx_buff,"%02X, ",res[i]);
HAL_UART_Transmit(&huart2, tx_buff, strlen(tx_buff), 100);
}
HAL_UART_Transmit(&huart2, "\n", 1, 100);
}else{
HAL_UART_Transmit(&huart2, "CRC16 ERROR!\n", 13, 100);
}
}
}
}
osDelay(1);
}
/* USER CODE END 5 */
}
void StartTask02(void *argument)
{
/* USER CODE BEGIN StartTask02 */
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == 0){
r_reg[0]--;
osDelay(200);
}
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == 0){
r_reg[0]++;
osDelay(200);
}
osDelay(1); //반드시 있어야함
}
/* USER CODE END StartTask02 */
}
void StartTask03(void *argument)
{
/* USER CODE BEGIN StartTask03 */
HAL_ADC_Start(&hadc1); //딱 한번만 호출
/* Infinite loop */
for(;;)
{
if(HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK)
{
r_reg[1] = HAL_ADC_GetValue(&hadc1); //0~4095
}
osDelay(100);
}
/* USER CODE END StartTask03 */
}
ModbusSerialMaster msm;
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
//접속버튼 클릭
serialPort1.PortName = textBox1.Text;
serialPort1.BaudRate = 9600;
serialPort1.Open();
if (serialPort1.IsOpen)
{
msm = ModbusSerialMaster.CreateRtu(serialPort1);
MessageBox.Show("접속완료");
}
else
{
MessageBox.Show("접속실패");
}
}
private void button2_Click(object sender, EventArgs e)
{
timer1.Start();
}
private void timer1_Tick(object sender, EventArgs e)
{
//타이머인터럽트가 0.1초마다 뭐하면되는냐?
//0번지부터 2개를 읽는다!
ushort[] data = msm.ReadInputRegisters(1, 0, 2);
textBox4.Text = ((short)data[0]).ToString();
textBox2.Text = ((short)data[1]).ToString();
}

