| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (C) Marvell International Ltd. and its affiliates |
| */ |
| |
| #include <common.h> |
| #include <i2c.h> |
| #include <spl.h> |
| #include <asm/io.h> |
| #include <asm/arch/cpu.h> |
| #include <asm/arch/soc.h> |
| |
| #include "ddr3_hw_training.h" |
| #include "xor.h" |
| #include "xor_regs.h" |
| |
| static void ddr3_flush_l1_line(u32 line); |
| |
| extern u32 pbs_pattern[2][LEN_16BIT_PBS_PATTERN]; |
| extern u32 pbs_pattern_32b[2][LEN_PBS_PATTERN]; |
| #if defined(MV88F78X60) |
| extern u32 pbs_pattern_64b[2][LEN_PBS_PATTERN]; |
| #endif |
| extern u32 pbs_dq_mapping[PUP_NUM_64BIT][DQ_NUM]; |
| |
| #if defined(MV88F78X60) || defined(MV88F672X) |
| /* PBS locked dq (per pup) */ |
| u32 pbs_locked_dq[MAX_PUP_NUM][DQ_NUM] = { { 0 } }; |
| u32 pbs_locked_dm[MAX_PUP_NUM] = { 0 }; |
| u32 pbs_locked_value[MAX_PUP_NUM][DQ_NUM] = { { 0 } }; |
| |
| int per_bit_data[MAX_PUP_NUM][DQ_NUM]; |
| #endif |
| |
| static u32 sdram_data[LEN_KILLER_PATTERN] __aligned(32) = { 0 }; |
| |
| static struct crc_dma_desc dma_desc __aligned(32) = { 0 }; |
| |
| #define XOR_TIMEOUT 0x8000000 |
| |
| struct xor_channel_t { |
| struct crc_dma_desc *desc; |
| unsigned long desc_phys_addr; |
| }; |
| |
| #define XOR_CAUSE_DONE_MASK(chan) ((0x1 | 0x2) << (chan * 16)) |
| |
| void xor_waiton_eng(int chan) |
| { |
| int timeout; |
| |
| timeout = 0; |
| while (!(reg_read(XOR_CAUSE_REG(XOR_UNIT(chan))) & |
| XOR_CAUSE_DONE_MASK(XOR_CHAN(chan)))) { |
| if (timeout > XOR_TIMEOUT) |
| goto timeout; |
| |
| timeout++; |
| } |
| |
| timeout = 0; |
| while (mv_xor_state_get(chan) != MV_IDLE) { |
| if (timeout > XOR_TIMEOUT) |
| goto timeout; |
| |
| timeout++; |
| } |
| |
| /* Clear int */ |
| reg_write(XOR_CAUSE_REG(XOR_UNIT(chan)), |
| ~(XOR_CAUSE_DONE_MASK(XOR_CHAN(chan)))); |
| |
| timeout: |
| return; |
| } |
| |
| static int special_compare_pattern(u32 uj) |
| { |
| if ((uj == 30) || (uj == 31) || (uj == 61) || (uj == 62) || |
| (uj == 93) || (uj == 94) || (uj == 126) || (uj == 127)) |
| return 1; |
| |
| return 0; |
| } |
| |
| /* |
| * Compare code extracted as its used by multiple functions. This |
| * reduces code-size and makes it easier to maintain it. Additionally |
| * the code is not indented that much and therefore easier to read. |
| */ |
| static void compare_pattern_v1(u32 uj, u32 *pup, u32 *pattern, |
| u32 pup_groups, int debug_dqs) |
| { |
| u32 val; |
| u32 uk; |
| u32 var1; |
| u32 var2; |
| __maybe_unused u32 dq; |
| |
| if (((sdram_data[uj]) != (pattern[uj])) && (*pup != 0xFF)) { |
| for (uk = 0; uk < PUP_NUM_32BIT; uk++) { |
| val = CMP_BYTE_SHIFT * uk; |
| var1 = ((sdram_data[uj] >> val) & CMP_BYTE_MASK); |
| var2 = ((pattern[uj] >> val) & CMP_BYTE_MASK); |
| |
| if (var1 != var2) { |
| *pup |= (1 << (uk + (PUP_NUM_32BIT * |
| (uj % pup_groups)))); |
| |
| #ifdef MV_DEBUG_DQS |
| if (!debug_dqs) |
| continue; |
| |
| for (dq = 0; dq < DQ_NUM; dq++) { |
| val = uk + (PUP_NUM_32BIT * |
| (uj % pup_groups)); |
| if (((var1 >> dq) & 0x1) != |
| ((var2 >> dq) & 0x1)) |
| per_bit_data[val][dq] = 1; |
| else |
| per_bit_data[val][dq] = 0; |
| } |
| #endif |
| } |
| } |
| } |
| } |
| |
| static void compare_pattern_v2(u32 uj, u32 *pup, u32 *pattern) |
| { |
| u32 val; |
| u32 uk; |
| u32 var1; |
| u32 var2; |
| |
| if (((sdram_data[uj]) != (pattern[uj])) && (*pup != 0x3)) { |
| /* Found error */ |
| for (uk = 0; uk < PUP_NUM_32BIT; uk++) { |
| val = CMP_BYTE_SHIFT * uk; |
| var1 = (sdram_data[uj] >> val) & CMP_BYTE_MASK; |
| var2 = (pattern[uj] >> val) & CMP_BYTE_MASK; |
| if (var1 != var2) |
| *pup |= (1 << (uk % PUP_NUM_16BIT)); |
| } |
| } |
| } |
| |
| /* |
| * Name: ddr3_sdram_compare |
| * Desc: Execute compare per PUP |
| * Args: unlock_pup Bit array of the unlock pups |
| * new_locked_pup Output bit array of the pups with failed compare |
| * pattern Pattern to compare |
| * pattern_len Length of pattern (in bytes) |
| * sdram_offset offset address to the SDRAM |
| * write write to the SDRAM before read |
| * mask compare pattern with mask; |
| * mask_pattern Mask to compare pattern |
| * |
| * Notes: |
| * Returns: MV_OK if success, other error code if fail. |
| */ |
| int ddr3_sdram_compare(MV_DRAM_INFO *dram_info, u32 unlock_pup, |
| u32 *new_locked_pup, u32 *pattern, |
| u32 pattern_len, u32 sdram_offset, int write, |
| int mask, u32 *mask_pattern, |
| int special_compare) |
| { |
| u32 uj; |
| __maybe_unused u32 pup_groups; |
| __maybe_unused u32 dq; |
| |
| #if !defined(MV88F67XX) |
| if (dram_info->num_of_std_pups == PUP_NUM_64BIT) |
| pup_groups = 2; |
| else |
| pup_groups = 1; |
| #endif |
| |
| ddr3_reset_phy_read_fifo(); |
| |
| /* Check if need to write to sdram before read */ |
| if (write == 1) |
| ddr3_dram_sram_burst((u32)pattern, sdram_offset, pattern_len); |
| |
| ddr3_dram_sram_burst(sdram_offset, (u32)sdram_data, pattern_len); |
| |
| /* Compare read result to write */ |
| for (uj = 0; uj < pattern_len; uj++) { |
| if (special_compare && special_compare_pattern(uj)) |
| continue; |
| |
| #if defined(MV88F78X60) || defined(MV88F672X) |
| compare_pattern_v1(uj, new_locked_pup, pattern, pup_groups, 1); |
| #elif defined(MV88F67XX) |
| compare_pattern_v2(uj, new_locked_pup, pattern); |
| #endif |
| } |
| |
| return MV_OK; |
| } |
| |
| #if defined(MV88F78X60) || defined(MV88F672X) |
| /* |
| * Name: ddr3_sdram_dm_compare |
| * Desc: Execute compare per PUP |
| * Args: unlock_pup Bit array of the unlock pups |
| * new_locked_pup Output bit array of the pups with failed compare |
| * pattern Pattern to compare |
| * pattern_len Length of pattern (in bytes) |
| * sdram_offset offset address to the SDRAM |
| * write write to the SDRAM before read |
| * mask compare pattern with mask; |
| * mask_pattern Mask to compare pattern |
| * |
| * Notes: |
| * Returns: MV_OK if success, other error code if fail. |
| */ |
| int ddr3_sdram_dm_compare(MV_DRAM_INFO *dram_info, u32 unlock_pup, |
| u32 *new_locked_pup, u32 *pattern, |
| u32 sdram_offset) |
| { |
| u32 uj, uk, var1, var2, pup_groups; |
| u32 val; |
| u32 pup = 0; |
| |
| if (dram_info->num_of_std_pups == PUP_NUM_64BIT) |
| pup_groups = 2; |
| else |
| pup_groups = 1; |
| |
| ddr3_dram_sram_burst((u32)pattern, SDRAM_PBS_TX_OFFS, |
| LEN_PBS_PATTERN); |
| ddr3_dram_sram_burst(SDRAM_PBS_TX_OFFS, (u32)sdram_data, |
| LEN_PBS_PATTERN); |
| |
| /* Validate the correctness of the results */ |
| for (uj = 0; uj < LEN_PBS_PATTERN; uj++) |
| compare_pattern_v1(uj, &pup, pattern, pup_groups, 0); |
| |
| /* Test the DM Signals */ |
| *(u32 *)(SDRAM_PBS_TX_OFFS + 0x10) = 0x12345678; |
| *(u32 *)(SDRAM_PBS_TX_OFFS + 0x14) = 0x12345678; |
| |
| sdram_data[0] = *(u32 *)(SDRAM_PBS_TX_OFFS + 0x10); |
| sdram_data[1] = *(u32 *)(SDRAM_PBS_TX_OFFS + 0x14); |
| |
| for (uj = 0; uj < 2; uj++) { |
| if (((sdram_data[uj]) != (pattern[uj])) && |
| (*new_locked_pup != 0xFF)) { |
| for (uk = 0; uk < PUP_NUM_32BIT; uk++) { |
| val = CMP_BYTE_SHIFT * uk; |
| var1 = ((sdram_data[uj] >> val) & CMP_BYTE_MASK); |
| var2 = ((pattern[uj] >> val) & CMP_BYTE_MASK); |
| if (var1 != var2) { |
| *new_locked_pup |= (1 << (uk + |
| (PUP_NUM_32BIT * (uj % pup_groups)))); |
| *new_locked_pup |= pup; |
| } |
| } |
| } |
| } |
| |
| return MV_OK; |
| } |
| |
| /* |
| * Name: ddr3_sdram_pbs_compare |
| * Desc: Execute SRAM compare per PUP and DQ. |
| * Args: pup_locked bit array of locked pups |
| * is_tx Indicate whether Rx or Tx |
| * pbs_pattern_idx Index of PBS pattern |
| * pbs_curr_val The PBS value |
| * pbs_lock_val The value to set to locked PBS |
| * skew_array Global array to update with the compare results |
| * ai_unlock_pup_dq_array bit array of the locked / unlocked pups per dq. |
| * Notes: |
| * Returns: MV_OK if success, other error code if fail. |
| */ |
| int ddr3_sdram_pbs_compare(MV_DRAM_INFO *dram_info, u32 pup_locked, |
| int is_tx, u32 pbs_pattern_idx, |
| u32 pbs_curr_val, u32 pbs_lock_val, |
| u32 *skew_array, u8 *unlock_pup_dq_array, |
| u32 ecc) |
| { |
| /* bit array failed dq per pup for current compare */ |
| u32 pbs_write_pup[DQ_NUM] = { 0 }; |
| u32 update_pup; /* pup as HW convention */ |
| u32 max_pup; /* maximal pup index */ |
| u32 pup_addr; |
| u32 ui, dq, pup; |
| int var1, var2; |
| u32 sdram_offset, pup_groups, tmp_pup; |
| u32 *pattern_ptr; |
| u32 val; |
| |
| /* Choose pattern */ |
| switch (dram_info->ddr_width) { |
| #if defined(MV88F672X) |
| case 16: |
| pattern_ptr = (u32 *)&pbs_pattern[pbs_pattern_idx]; |
| break; |
| #endif |
| case 32: |
| pattern_ptr = (u32 *)&pbs_pattern_32b[pbs_pattern_idx]; |
| break; |
| #if defined(MV88F78X60) |
| case 64: |
| pattern_ptr = (u32 *)&pbs_pattern_64b[pbs_pattern_idx]; |
| break; |
| #endif |
| default: |
| return MV_FAIL; |
| } |
| |
| max_pup = dram_info->num_of_std_pups; |
| |
| sdram_offset = SDRAM_PBS_I_OFFS + pbs_pattern_idx * SDRAM_PBS_NEXT_OFFS; |
| |
| if (dram_info->num_of_std_pups == PUP_NUM_64BIT) |
| pup_groups = 2; |
| else |
| pup_groups = 1; |
| |
| ddr3_reset_phy_read_fifo(); |
| |
| /* Check if need to write to sdram before read */ |
| if (is_tx == 1) { |
| ddr3_dram_sram_burst((u32)pattern_ptr, sdram_offset, |
| LEN_PBS_PATTERN); |
| } |
| |
| ddr3_dram_sram_read(sdram_offset, (u32)sdram_data, LEN_PBS_PATTERN); |
| |
| /* Compare read result to write */ |
| for (ui = 0; ui < LEN_PBS_PATTERN; ui++) { |
| if ((sdram_data[ui]) != (pattern_ptr[ui])) { |
| /* found error */ |
| /* error in low pup group */ |
| for (pup = 0; pup < PUP_NUM_32BIT; pup++) { |
| val = CMP_BYTE_SHIFT * pup; |
| var1 = ((sdram_data[ui] >> val) & |
| CMP_BYTE_MASK); |
| var2 = ((pattern_ptr[ui] >> val) & |
| CMP_BYTE_MASK); |
| |
| if (var1 != var2) { |
| if (dram_info->ddr_width > 16) { |
| tmp_pup = (pup + PUP_NUM_32BIT * |
| (ui % pup_groups)); |
| } else { |
| tmp_pup = (pup % PUP_NUM_16BIT); |
| } |
| |
| update_pup = (1 << tmp_pup); |
| if (ecc && (update_pup != 0x1)) |
| continue; |
| |
| /* |
| * Pup is failed - Go over all DQs and |
| * look for failures |
| */ |
| for (dq = 0; dq < DQ_NUM; dq++) { |
| val = tmp_pup * (1 - ecc) + |
| ecc * ECC_PUP; |
| if (((var1 >> dq) & 0x1) != |
| ((var2 >> dq) & 0x1)) { |
| if (pbs_locked_dq[val][dq] == 1 && |
| pbs_locked_value[val][dq] != pbs_curr_val) |
| continue; |
| |
| /* |
| * Activate write to |
| * update PBS to |
| * pbs_lock_val |
| */ |
| pbs_write_pup[dq] |= |
| update_pup; |
| |
| /* |
| * Update the |
| * unlock_pup_dq_array |
| */ |
| unlock_pup_dq_array[dq] &= |
| ~update_pup; |
| |
| /* |
| * Lock PBS value for |
| * failed bits in |
| * compare operation |
| */ |
| skew_array[tmp_pup * DQ_NUM + dq] = |
| pbs_curr_val; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| pup_addr = (is_tx == 1) ? PUP_PBS_TX : PUP_PBS_RX; |
| |
| /* Set last failed bits PBS to min / max pbs value */ |
| for (dq = 0; dq < DQ_NUM; dq++) { |
| for (pup = 0; pup < max_pup; pup++) { |
| if (pbs_write_pup[dq] & (1 << pup)) { |
| val = pup * (1 - ecc) + ecc * ECC_PUP; |
| if (pbs_locked_dq[val][dq] == 1 && |
| pbs_locked_value[val][dq] != pbs_curr_val) |
| continue; |
| |
| /* Mark the dq as locked */ |
| pbs_locked_dq[val][dq] = 1; |
| pbs_locked_value[val][dq] = pbs_curr_val; |
| ddr3_write_pup_reg(pup_addr + |
| pbs_dq_mapping[val][dq], |
| CS0, val, 0, pbs_lock_val); |
| } |
| } |
| } |
| |
| return MV_OK; |
| } |
| #endif |
| |
| /* |
| * Name: ddr3_sdram_direct_compare |
| * Desc: Execute compare per PUP without DMA (no burst mode) |
| * Args: unlock_pup Bit array of the unlock pups |
| * new_locked_pup Output bit array of the pups with failed compare |
| * pattern Pattern to compare |
| * pattern_len Length of pattern (in bytes) |
| * sdram_offset offset address to the SDRAM |
| * write write to the SDRAM before read |
| * mask compare pattern with mask; |
| * auiMaskPatter Mask to compare pattern |
| * |
| * Notes: |
| * Returns: MV_OK if success, other error code if fail. |
| */ |
| int ddr3_sdram_direct_compare(MV_DRAM_INFO *dram_info, u32 unlock_pup, |
| u32 *new_locked_pup, u32 *pattern, |
| u32 pattern_len, u32 sdram_offset, |
| int write, int mask, u32 *mask_pattern) |
| { |
| u32 uj, uk, pup_groups; |
| u32 *sdram_addr; /* used to read from SDRAM */ |
| |
| sdram_addr = (u32 *)sdram_offset; |
| |
| if (dram_info->num_of_std_pups == PUP_NUM_64BIT) |
| pup_groups = 2; |
| else |
| pup_groups = 1; |
| |
| /* Check if need to write before read */ |
| if (write == 1) { |
| for (uk = 0; uk < pattern_len; uk++) { |
| *sdram_addr = pattern[uk]; |
| sdram_addr++; |
| } |
| } |
| |
| sdram_addr = (u32 *)sdram_offset; |
| |
| for (uk = 0; uk < pattern_len; uk++) { |
| sdram_data[uk] = *sdram_addr; |
| sdram_addr++; |
| } |
| |
| /* Compare read result to write */ |
| for (uj = 0; uj < pattern_len; uj++) { |
| if (dram_info->ddr_width > 16) { |
| compare_pattern_v1(uj, new_locked_pup, pattern, |
| pup_groups, 0); |
| } else { |
| compare_pattern_v2(uj, new_locked_pup, pattern); |
| } |
| } |
| |
| return MV_OK; |
| } |
| |
| /* |
| * Name: ddr3_dram_sram_burst |
| * Desc: Read from the SDRAM in burst of 64 bytes |
| * Args: src |
| * dst |
| * Notes: Using the XOR mechanism |
| * Returns: MV_OK if success, other error code if fail. |
| */ |
| int ddr3_dram_sram_burst(u32 src, u32 dst, u32 len) |
| { |
| u32 chan, byte_count, cs_num, byte; |
| struct xor_channel_t channel; |
| |
| chan = 0; |
| byte_count = len * 4; |
| |
| /* Wait for previous transfer completion */ |
| while (mv_xor_state_get(chan) != MV_IDLE) |
| ; |
| |
| /* Build the channel descriptor */ |
| channel.desc = &dma_desc; |
| |
| /* Enable Address Override and set correct src and dst */ |
| if (src < SRAM_BASE) { |
| /* src is DRAM CS, dst is SRAM */ |
| cs_num = (src / (1 + SDRAM_CS_SIZE)); |
| reg_write(XOR_ADDR_OVRD_REG(0, 0), |
| ((cs_num << 1) | (1 << 0))); |
| channel.desc->src_addr0 = (src % (1 + SDRAM_CS_SIZE)); |
| channel.desc->dst_addr = dst; |
| } else { |
| /* src is SRAM, dst is DRAM CS */ |
| cs_num = (dst / (1 + SDRAM_CS_SIZE)); |
| reg_write(XOR_ADDR_OVRD_REG(0, 0), |
| ((cs_num << 25) | (1 << 24))); |
| channel.desc->src_addr0 = (src); |
| channel.desc->dst_addr = (dst % (1 + SDRAM_CS_SIZE)); |
| channel.desc->src_addr0 = src; |
| channel.desc->dst_addr = (dst % (1 + SDRAM_CS_SIZE)); |
| } |
| |
| channel.desc->src_addr1 = 0; |
| channel.desc->byte_cnt = byte_count; |
| channel.desc->next_desc_ptr = 0; |
| channel.desc->status = 1 << 31; |
| channel.desc->desc_cmd = 0x0; |
| channel.desc_phys_addr = (unsigned long)&dma_desc; |
| |
| ddr3_flush_l1_line((u32)&dma_desc); |
| |
| /* Issue the transfer */ |
| if (mv_xor_transfer(chan, MV_DMA, channel.desc_phys_addr) != MV_OK) |
| return MV_FAIL; |
| |
| /* Wait for completion */ |
| xor_waiton_eng(chan); |
| |
| if (dst > SRAM_BASE) { |
| for (byte = 0; byte < byte_count; byte += 0x20) |
| cache_inv(dst + byte); |
| } |
| |
| return MV_OK; |
| } |
| |
| /* |
| * Name: ddr3_flush_l1_line |
| * Desc: |
| * Args: |
| * Notes: |
| * Returns: MV_OK if success, other error code if fail. |
| */ |
| static void ddr3_flush_l1_line(u32 line) |
| { |
| u32 reg; |
| |
| #if defined(MV88F672X) |
| reg = 1; |
| #else |
| reg = reg_read(REG_SAMPLE_RESET_LOW_ADDR) & |
| (1 << REG_SAMPLE_RESET_CPU_ARCH_OFFS); |
| #ifdef MV88F67XX |
| reg = ~reg & (1 << REG_SAMPLE_RESET_CPU_ARCH_OFFS); |
| #endif |
| #endif |
| |
| if (reg) { |
| /* V7 Arch mode */ |
| flush_l1_v7(line); |
| flush_l1_v7(line + CACHE_LINE_SIZE); |
| } else { |
| /* V6 Arch mode */ |
| flush_l1_v6(line); |
| flush_l1_v6(line + CACHE_LINE_SIZE); |
| } |
| } |
| |
| int ddr3_dram_sram_read(u32 src, u32 dst, u32 len) |
| { |
| u32 ui; |
| u32 *dst_ptr, *src_ptr; |
| |
| dst_ptr = (u32 *)dst; |
| src_ptr = (u32 *)src; |
| |
| for (ui = 0; ui < len; ui++) { |
| *dst_ptr = *src_ptr; |
| dst_ptr++; |
| src_ptr++; |
| } |
| |
| return MV_OK; |
| } |
| |
| int ddr3_sdram_dqs_compare(MV_DRAM_INFO *dram_info, u32 unlock_pup, |
| u32 *new_locked_pup, u32 *pattern, |
| u32 pattern_len, u32 sdram_offset, int write, |
| int mask, u32 *mask_pattern, |
| int special_compare) |
| { |
| u32 uj, pup_groups; |
| |
| if (dram_info->num_of_std_pups == PUP_NUM_64BIT) |
| pup_groups = 2; |
| else |
| pup_groups = 1; |
| |
| ddr3_reset_phy_read_fifo(); |
| |
| /* Check if need to write to sdram before read */ |
| if (write == 1) |
| ddr3_dram_sram_burst((u32)pattern, sdram_offset, pattern_len); |
| |
| ddr3_dram_sram_burst(sdram_offset, (u32)sdram_data, pattern_len); |
| |
| /* Compare read result to write */ |
| for (uj = 0; uj < pattern_len; uj++) { |
| if (special_compare && special_compare_pattern(uj)) |
| continue; |
| |
| if (dram_info->ddr_width > 16) { |
| compare_pattern_v1(uj, new_locked_pup, pattern, |
| pup_groups, 1); |
| } else { |
| compare_pattern_v2(uj, new_locked_pup, pattern); |
| } |
| } |
| |
| return MV_OK; |
| } |
| |
| void ddr3_reset_phy_read_fifo(void) |
| { |
| u32 reg; |
| |
| /* reset read FIFO */ |
| reg = reg_read(REG_DRAM_TRAINING_ADDR); |
| /* Start Auto Read Leveling procedure */ |
| reg |= (1 << REG_DRAM_TRAINING_RL_OFFS); |
| |
| /* 0x15B0 - Training Register */ |
| reg_write(REG_DRAM_TRAINING_ADDR, reg); |
| |
| reg = reg_read(REG_DRAM_TRAINING_2_ADDR); |
| reg |= ((1 << REG_DRAM_TRAINING_2_FIFO_RST_OFFS) + |
| (1 << REG_DRAM_TRAINING_2_SW_OVRD_OFFS)); |
| |
| /* [0] = 1 - Enable SW override, [4] = 1 - FIFO reset */ |
| /* 0x15B8 - Training SW 2 Register */ |
| reg_write(REG_DRAM_TRAINING_2_ADDR, reg); |
| |
| do { |
| reg = reg_read(REG_DRAM_TRAINING_2_ADDR) & |
| (1 << REG_DRAM_TRAINING_2_FIFO_RST_OFFS); |
| } while (reg); /* Wait for '0' */ |
| |
| reg = reg_read(REG_DRAM_TRAINING_ADDR); |
| |
| /* Clear Auto Read Leveling procedure */ |
| reg &= ~(1 << REG_DRAM_TRAINING_RL_OFFS); |
| |
| /* 0x15B0 - Training Register */ |
| reg_write(REG_DRAM_TRAINING_ADDR, reg); |
| } |